diff --git a/.gitignore b/.gitignore index b25bde8401..4696b98cac 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,6 @@ packages/vchart/__tests__/runtime/node/**.png *.tsbuildinfo .github/hooks/copilot-hooks.json .omx/ + +# Local visual regression artifacts +.vchart-visual/ diff --git a/AGENTS.md b/AGENTS.md index 56ead02a34..3d3b172b41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,3 +24,12 @@ VChart 是高性能可视化渲染库。工程取舍应优先面向正确、文 2. render、update、interaction、animation、release 的高性能。 3. 稳定且易用的公共调用方式。 4. 最小化兼容性兜底,只在真实 API 承诺需要时引入。 + +## 本地视觉回归测试 + +- 开发完成后,依据改动运行相关目录或单用例;影响公共渲染、布局、数据或状态路径时考虑多个目录,不确定范围时运行全量。命令从仓库根目录执行:`node packages/vchart/scripts/visual-test.mjs --dir components/label` 或 `--case pie-label`。 +- 每次提交 PR 前运行全量:`node packages/vchart/scripts/visual-test.mjs`。默认比较当前工作区(含未提交修改)与官方 develop。`--self-compare` 仅验证稳定性,不能代替基线比较。 +- 查看三图 HTML 或 `agent-summary.md`:视觉差异需要审查和说明,执行错误必须解决;无法运行、基线不支持新功能等阻断应如实记录,不得写成通过,不跳过用例或放宽阈值。 +- 新增功能、修复 bug 或发现覆盖空缺时,补充有明确目的的确定性回归用例,或补强相关用例;交互必须断言真实状态变化。登记到显式清单并按用例指南完成稳定性及反例检查。 +- 开发者或 Agent 独立新增用例不填写 `BugServer case IDs` 行,不填空值或占位 ID。现有迁移项保留真实来源,改写保留原 ID、合并保留多个。未来同步到 BugServer 成功后再补齐真实 ID;本期没有同步功能。 +- 使用 Node.js 22;环境准备、全量/目录/单例命令、报告及清理见 [视觉测试说明](./packages/vchart/__tests__/visual/README.md),目录与编写规范见 [用例指南](./packages/vchart/__tests__/visual/cases/README.md)。测试不替代必要的单元、性能及发版前全量测试。 diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 917bef60ae..d15831adf6 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -571,6 +571,9 @@ importers: '@internal/typescript-json-schema': specifier: workspace:* version: link:../../tools/typescript-json-schema + '@playwright/test': + specifier: 1.63.0 + version: 1.63.0 '@rushstack/eslint-patch': specifier: ~1.1.4 version: 1.1.4 @@ -1255,7 +1258,7 @@ importers: version: 4.9.5 vitest: specifier: 0.30.1 - version: 0.30.1(jsdom@16.7.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.32.11)(stylus@0.54.8)(sugarss@2.0.0)(terser@5.17.1) + version: 0.30.1(jsdom@16.7.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(playwright@1.63.0)(sass@1.32.11)(stylus@0.54.8)(sugarss@2.0.0)(terser@5.17.1) ../../tools/story-player: dependencies: @@ -2352,6 +2355,11 @@ packages: engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + '@pmmmwh/react-refresh-webpack-plugin@0.4.3': resolution: {integrity: sha512-br5Qwvh8D2OQqSXpd1g/xqXKnK0r+Jz6qVKBbWmpUcrbGOxUrf39V5oZ1876084CGn18uMdR5uvPqBv9UqtBjQ==} engines: {node: '>= 10.x'} @@ -9440,6 +9448,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} @@ -13892,6 +13910,10 @@ snapshots: mkdirp: 1.0.4 rimraf: 3.0.2 + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + '@pmmmwh/react-refresh-webpack-plugin@0.4.3(react-refresh@0.9.0)(sockjs-client@1.4.0)(type-fest@0.13.1)(webpack-dev-server@3.11.0(webpack@4.46.0))(webpack@4.46.0)': dependencies: ansi-html: 0.0.7 @@ -23475,6 +23497,12 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + please-upgrade-node@3.2.0: dependencies: semver-compare: 1.0.0 @@ -26589,7 +26617,7 @@ snapshots: sugarss: 2.0.0 terser: 5.17.1 - vitest@0.30.1(jsdom@16.7.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.32.11)(stylus@0.54.8)(sugarss@2.0.0)(terser@5.17.1): + vitest@0.30.1(jsdom@16.7.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(playwright@1.63.0)(sass@1.32.11)(stylus@0.54.8)(sugarss@2.0.0)(terser@5.17.1): dependencies: '@types/chai': 4.3.20 '@types/chai-subset': 1.3.6(@types/chai@4.3.20) @@ -26619,6 +26647,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: jsdom: 16.7.0(canvas@2.11.2(encoding@0.1.13)) + playwright: 1.63.0 transitivePeerDependencies: - less - sass diff --git a/packages/vchart/__tests__/visual/DESIGN.md b/packages/vchart/__tests__/visual/DESIGN.md new file mode 100644 index 0000000000..ab3583c471 --- /dev/null +++ b/packages/vchart/__tests__/visual/DESIGN.md @@ -0,0 +1,48 @@ +# 本地视觉测试工具设计 + +工具服务 VChart 核心包的本地开发,以同一批冻结用例比较当前工作区与官方 develop 固定提交的产物。命令和结果协议见 [运行说明](./README.md),当前平台验证范围见 [验收摘要](./cases/ACCEPTANCE.md)。 + +## 边界 + +只提供全量、目录、单用例三种选择,由开发者或 Agent 判断相关范围;不自动推断代码影响,不提供永久截图基线或接受差异命令。不接入线上 case、CI、多产品框架或模型服务。默认比较需要公开 GitHub 和依赖下载;准备完成后的自比较不获取远端基线,报告可离线阅读。 + +## 职责 + +| 文件(相对于 packages/vchart) | 职责 | +| --------------------------------------------------------- | ------------------------------------------------- | +| `scripts/visual-test.mjs` | CLI 参数、入口及生命周期 | +| `scripts/visual/runner.mjs` | 清单校验、选择、冻结、预检、阶段执行及结果校验 | +| `scripts/visual/build.mjs` | 官方基线、独立 worktree、最小构建链与缓存 | +| `scripts/visual/runtime.mjs` | 子进程、回环服务、文件摘要与清理 | +| `__tests__/visual/cases/index.mjs` | 唯一可执行清单,模块契约见用例指南 | +| `__tests__/visual/helpers.mjs`、`interaction-helpers.mjs` | 共享绘制、配置、图元定位和状态检查 | +| `__tests__/visual/page.html`、`visual.spec.mjs` | 加载指定产物、渲染、交互、验证及最终截图 | +| `__tests__/visual/settings.mjs`、`playwright.config.mjs` | 固定环境、单 worker、超时及比较策略 | +| `__tests__/visual/reporter.mjs`、`report.mjs` | 增量阶段记录、结果归一化、三图 HTML 与 Agent 报告 | + +沿用 `.mjs`、JSDoc 和现有依赖,不增加单独编译链。模块导入不注册信号或启动进程;信号由运行入口管理。 + +## 执行与隔离 + +1. 校验参数和环境,建立运行目录及独占锁,记录 runId、PID、开始时间;未知锁不夺取。 +2. 递归冻结当前用例、页面、配置和测试代码,从副本加载清单并固定选择集,记录文件摘要。 +3. 从固定官方地址取得 develop 或指定完整 SHA,不依赖 origin、不以旧缓存代替拉取成功。 +4. 本地使用已有依赖并包含未提交修改;基线用独立 detached worktree,按其锁文件安装。两侧不共享安装目录。 +5. 两侧执行同一最小构建链:内部 bundler、vutils-extension ES、VChart UMD;关闭额外入口与 postTasks。仅清理已知生成路径,检查新产物非空并复制记录摘要。本地构建前后 HEAD 或工作区内容变化则失败。 +6. 通过随机端口的回环 HTTP 服务加载冻结输入。每例新 context,公共绘制检查、动作和最终状态断言、字体就绪及稳定截图后才比较。 +7. 基线全部成功后执行本地阶段,本地禁止生成缺失基线。按精确 ID 集合与完整附件校验结果;缺失、重复或额外结果均失败。 +8. 正常、失败或信号路径逐项清理本次资源;保留原始错误与清理错误,再生成最终 JSON、HTML、Markdown 和退出码。 + +自比较只构建本地一次,在两侧隔离 context 执行;验证确定性,不证明图表内容正确。用例从页面注入的 VChart 获取实例,不能导入工作区源码而绕过指定产物。截图阶段禁止外网请求;环境与阈值统一配置,不开放放宽阈值或跳过断言的 CLI 参数。 + +## 缓存与清理 + +仅缓存最近一次成功基线构建,键包含 SHA、锁文件、Node、平台架构及实际构建配方摘要;报告样式变化不使构建缓存失效。内容摘要错误则重建,权限和磁盘错误明确失败。准备完新缓存再替换旧缓存,每次重新截图;本地每次重建。 + +SIGINT/SIGTERM 只终止本次进程组并清理浏览器、服务、worktree 和锁。SIGKILL 或断电后人工确认遗留资源,不能终止其他任务。输出集中于忽略目录 `.vchart-visual/`,不提交截图、报告或缓存。 + +## 结果与扩展原则 + +`summary.json` 的 `schemaVersion: 1` 是 HTML 与 Agent Markdown 的共同数据源;保留阶段结果、诊断码、环境、摘要和相对证据路径。`finalized` 表示终态,`complete` 表示所有选中用例完成有效比较,两者独立。完整比较无差异为 0,有差异为 1,执行、证据、清理或报告失败为 2。截图断言失败只有在比较证据完整时才能归为差异。 + +新增 case 遵循 [用例指南](./cases/README.md),来源 ID 可选且不影响执行。同步到 BugServer 仅为后续计划,本期没有同步状态、任务或命令。修改工具时使用现有 Node 测试入口,故障注入仅操作独立验证目录;单元测试、性能测试和内网发版全量测试继续承担各自职责。 diff --git a/packages/vchart/__tests__/visual/README.md b/packages/vchart/__tests__/visual/README.md new file mode 100644 index 0000000000..0b3a8cf3bb --- /dev/null +++ b/packages/vchart/__tests__/visual/README.md @@ -0,0 +1,151 @@ +# 本地视觉回归测试工具 + +工具结构见 [设计说明](./DESIGN.md),用例选择与新增见 [用例指南](./cases/README.md),平台实测范围见 [验收摘要](./cases/ACCEPTANCE.md)。 + +使用同一批精简用例,在本机分别运行当前工作区与官方 `VisActor/VChart` 的 develop 构建,生成截图和差异报告。无需内网 BugServer 或访问凭证。图片差异表示需要检查,不等同于缺陷;两侧共同存在的错误仍需其他测试发现。 + +## 准备 + +使用 Node.js 22。macOS(arm64)与 Linux 验收通过,Linux 实测环境为 veLinux 2(x64)。Ubuntu 未独立实测,不阻塞本轮交付。Linux 必须有 Chromium 所需系统库。先在仓库根目录完成依赖安装: + +```sh +node common/scripts/install-run-rush.js install --ignore-hooks +``` + +安装与锁文件对应的 Chromium: + +```sh +# macOS +node packages/vchart/node_modules/@playwright/test/cli.js install chromium --no-remove + +# Linux:系统依赖安装可能需要管理员权限 +node packages/vchart/node_modules/@playwright/test/cli.js install --with-deps chromium --no-remove +``` + +Rush 安装还需要项目现有的 node-canvas 编译依赖;按仓库贡献指南准备。截图命令本身不安装系统软件、不启动 Docker。 + +## 使用 + +在仓库根目录运行: + +```sh +node packages/vchart/scripts/visual-test.mjs +node packages/vchart/scripts/visual-test.mjs --baseline <完整的40位commit-sha> +node packages/vchart/scripts/visual-test.mjs --dir components +node packages/vchart/scripts/visual-test.mjs --dir components/label +node packages/vchart/scripts/visual-test.mjs --case pie-label +node packages/vchart/scripts/visual-test.mjs --self-compare +node packages/vchart/scripts/visual-test.mjs --list +node packages/vchart/scripts/visual-test.mjs --check +``` + +不传选择参数运行全部本地 case;`--dir` 递归运行 cases 内的一个目录,`--case` 按 ID 运行一个用例。目录和 ID 互斥,不根据源码改动自动追加测试。目录使用小写字母、数字、连字符和 `/`,例如 `components/label`;不接受绝对路径、`..`、glob、空路径或重复 `--dir`。不存在或没有已登记 case 的目录返回 `2`,不会回退为全量。 + +三种范围均可搭配 `--baseline ` 或 `--self-compare`(两者互斥)。目录只表示用例归属,选择哪些测试由开发者或 Agent 判断;参见 [用例目录导航](./cases/README.md)。目录模式减少截图与交互数量,本地构建仍每次执行。 + +`--list` 独立列出用例元数据,不需要浏览器。`--check` 独立检查 Node、Git、当前依赖和 Chromium 实际启动,检查后关闭浏览器;不自动安装、不构建、不截图。预检日志位于 `.vchart-visual/preflight.log`。帮助不要求先安装 Playwright。 + +包目录内也可使用 `rushx test:visual`。`--help` 列出选项;不存在更新永久基线或接受差异的命令。 + +- 默认每次从官方仓库获取 develop,并固定本次 SHA;fork 的 `origin` 不影响基线选择。断网或拉取失败会报错,缓存不会冒充最新版本。 +- 指定 SHA 仍从官方仓库获取该提交;用于复现已知基线。首期不支持任意仓库或共同祖先自动选择。 +- 当前工作区包含未提交修改,每次重新构建;构建过程中源码发生变化会要求重跑。 +- 基线按自己的锁文件独立安装和构建。只缓存最近一次成功的基线构建,每次重新截图;缓存失效由 SHA、锁文件、Node、系统架构和构建配方决定;报告样式修改不使缓存失效,内容损坏会重建。 +- 自比较只构建本地一次,在两套隔离 context 中执行;它验证测试确定性,不能证明图表结果正确。 + +退出码:`0` 无差异;`1` 有视觉差异;`2` 参数、构建、执行、资源、超时、缺图或清理错误。多个问题同时出现时执行错误优先。 + +## 报告与清理 + +命令输出 `.vchart-visual/runs//index.html`,浏览器直接打开即可离线查看,无需启动报告服务: + +- 每个用例展示 **基线 / 本地 / 差异** 三列图片,点击图片打开原始分辨率;通过用例的差异列显示“无视觉差异”。 +- 显示本次范围、选中/总量及范围复现命令;未选用例不计为通过或未完成。汇总通过、视觉差异、执行错误、未完成数量;默认展示问题用例,可按状态和用例筛选。 +- 展示用例目的、实际用例源码行号、冻结用例链接、阶段诊断和单用例复现命令。指定基线 SHA 固定,但复现本地结果仍需要相同工作区修改。 +- 缺图明确显示“未生成或缺失”;已完成截图的结果丢失必要图片时,汇总升级为执行错误,CLI 返回 `2`。 +- 分享报告时复制整个运行目录,保留相对目录结构;单独复制 HTML 不包含图片。页面不请求外网。 + +同一次运行同时生成 `agent-summary.md` 和 `summary.json`,与 HTML 共用一份归一化结果: + +| 文件 / 字段 | 作用 | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| `agent-summary.md` | Agent 首选入口:运行环境、失败/未完成用例、诊断、三图路径与复现命令;通过用例仅汇总 | +| `summary.json` / `schemaVersion: 1` | 版本化结构化结果,完整保存全部用例及证据路径,不嵌入图片 Base64 | +| `status` / `complete` / `counts` | 运行结论、是否完成所选用例的有效比较、四类用例计数;运行级错误仍可使已完成比较的运行失败 | +| `selection` / `rerun` | 本次模式(all/directory/case)、选择值、选中 ID、选中/总量与绑定基线的范围复现命令 | +| `baseline` / `local` / `environment` / `timings` | 基线 SHA、本地 HEAD/dirty/修改摘要、运行时与浏览器版本、阶段耗时 | +| `cases[].source` | `path` 相对于仓库根目录;`line` 指向冻结模块入口;`frozenPath` 相对于报告目录 | +| `cases[].phases` / `errors` | 两侧状态及错误的 `phase`、`stage`、`category`、原始诊断文本 | +| `cases[].images` / `attachments` / `rerun` | 三图相对路径(缺失为 `null`)、截图/trace 等证据路径、仓库根目录复现命令 | +| `issues` / `logs` | 构建、中断、清理等运行级错误,以及实际存在的日志和原生报告路径 | + +用例状态为 `passed`、`diff`、`error`、`not_run`;运行状态为前三种。错误类别包括 `visual_difference`、`missing_artifact`、`timeout`、`resource`、`render`、`interaction`、`screenshot`、`comparison`、`setup`,以及运行级 `execution` / `report`。Agent 应先读取摘要,再按需读取 JSON、源码、图片或日志;报告只给出事实,不推断根因、不放宽阈值、不自动接受差异,也不调用任何模型 API。 + +基线和本地阶段的 Playwright HTML/JSON 报告仍保留用于详细调试。失败阶段可能没有完整原生 HTML,汇总页只链接实际存在的报告与日志。 + +需要交互查看 Playwright 报告时,显式运行: + +```sh +node packages/vchart/node_modules/@playwright/test/cli.js show-report .vchart-visual/runs//current-report +``` + +查看完成后 Ctrl+C 退出报告服务。测试命令不会自动启动此常驻服务。 + +正常结束、失败和 Ctrl+C 会回收测试子进程、浏览器、HTTP 服务及临时 worktree。报告保留供检查。SIGKILL 或断电无法执行清理:先确认无测试进程,再检查 `git worktree list`、删除该次临时 worktree 和 `.vchart-visual/running.lock` 后重跑。请勿删除其他任务的 worktree。 + +`.vchart-visual/` 已被 Git 忽略。确认没有测试运行后,可删除旧 `runs` 释放磁盘空间;不要提交截图,也不要把基线替换为待测图片。 + +## 用例约定 + +当前 256 个本地用例在 `cases/index.mjs` 显式注册,一用例一文件,按主要验证目的归入 `charts/`、`components/`、`data/`、`layout/`、`theme/`、`interaction/` 和 `api/`。详见 [目录导航、用例说明和待补场景](./cases/README.md)。元数据仅在清单中维护,每项包含 `id`、`purpose`、`file`,`sourceExample` 为可选的真实公开示例来源。 + +ID 必须唯一且符合 `^[a-z][a-z0-9-]*$`;file 使用 cases 内的 `./<目录>/.mjs`,目录层级和文件名使用小写字母、数字与连字符。缺文件、非法导出、越界、符号链接和空选择集在构建前失败,未登记的模块不会自动执行。 + +用例模块默认导出 `createSpec()`、可选 `exercise(page)`、必需 `verify(page)`。核心函数补中文说明。新增用例可复制以下结构: + +```js +export default { + createSpec() { + // 固定输入,每次创建新对象。 + return { type: 'bar', data: { values: [{ x: 'A', y: 10 }] }, xField: 'x', yField: 'y' }; + }, + async verify(page) { + // 检查公开 spec,图片比较负责布局呈现。 + await page.evaluate(() => { + if (window.__visualChart.getSpec().type !== 'bar') throw new Error('图表类型不正确'); + }); + } +}; +``` + +在清单中注册模块,存在真实公开示例时填写来源,然后运行 `--list`、`--check`、`--self-compare --case ` 及默认基线单用例比较。开发者或 Agent 独立新增的用例不填写 `BugServer case IDs` 行,不填写空值、占位或虚构 ID。既有 BugServer 迁移项保留真实 ID,改写保留原 ID、合并列出多个;后续独立新增项待同步成功后再补齐服务返回的 ID,本期没有同步功能。不得将内部业务内容直接带入公开代码。新增 case 顶部说明验证目的、图表类型、关键配置、场景条件、最终检查和覆盖边界;不修改原始调试示例。 + +`createSpec()` 不能导入本地 VChart 源码、Node API 或测试框架运行时代码,也不能加载网络数据。页面只加载指定产物;两侧共用冻结副本。`exercise()` 执行动作,`verify()` 验证实际目标状态;不能只等待固定时间或只检查图片存在。共享的场景树定位在 `helpers.mjs`,定位不到目标必须失败。 + +静态验证核对明确指定的类型、数据及配置子集,允许 VChart 合并主题默认值;公共检查还要求有效画布、图元及绘制像素。图例、tooltip、缩放和更新尺寸分别验证真实状态改变。 + +确定性配置集中在 `settings.mjs`:单 worker、无重试、新 context、Chromium headless、1000×800、图表 800×600、DPR 1、白底、Arial、en-US、UTC、单用例 30 秒。关闭动画,等待字体与连续稳定截图;像素颜色阈值 0.1、允许差异像素数 0。固定数据和文本,来源中的通用中英文文字及显式富文本字体保留,不使用远程图片或字体;两侧在同机比较,不要求 macOS 与 Linux 像素一致。 + +## 结果与错误协议 + +`summary.json` 是 HTML 和 Agent 摘要的共同数据源。已有 schemaVersion=1 字段保持兼容,新增 `runId`、`finalized`、`request`、`comparison`、`frozenFiles`、分阶段耗时和诊断 `code`。未完成用例还记录 `blockedBy`。 + +`finalized` 表示运行已形成终态,`complete` 表示所选用例全部完成有效比较;清理失败可以 finalized=true、complete=true、status=error。执行中保留 finalized=false 的 JSON;每完成一个用例原子落盘阶段结果,异常退出不丢失已完成诊断。 + +稳定错误码包括 `INVALID_ARGUMENT`、`PREFLIGHT_FAILED`、`CASE_MANIFEST_INVALID`、`BASELINE_FETCH_FAILED`、`BUILD_FAILED`、`WORKSPACE_CHANGED`、`RESOURCE_FAILED`、`RENDER_FAILED`、`CASE_ASSERTION_FAILED`、`INTERACTION_ASSERTION_FAILED`、`SCREENSHOT_FAILED`、`COMPARISON_FAILED`、`TIMEOUT`、`MISSING_ARTIFACT`、`RESULT_SET_MISMATCH`、`RUN_LOCKED`、`RUN_INTERRUPTED`、`RUN_INCOMPLETE`、`CLEANUP_FAILED`、`REPORT_FAILED` 和兜底 `EXECUTION_FAILED`。只有完整的原生比较证据才会标为 `VISUAL_DIFFERENCE`。 + +准备失败尽可能生成报告。参数不可解析或输出不可写时可能只有 stderr,退出码仍是 2;报告写入失败时保留阶段证据并尽可能写入错误 JSON,不保留误导性的成功 HTML。原始错误文字供排查,Agent 应优先使用稳定错误码。 + +锁的 `owner.json` 记录 runId、PID、启动时间和目录;工具不自动夺锁。SIGINT/SIGTERM 逐项清理本次资源,主错误与清理错误同时保留。SIGKILL/断电后的锁需人工确认,不允许据此终止其他进程。 + +## 验证工具本身 + +先完成一次自比较以生成本地 UMD,再运行故障自检: + +```sh +node --test packages/vchart/scripts/visual-test.test.mjs +``` + +自检在独立临时目录中验证:相同产物通过、仅候选构建改变颜色产生差异、缺图不自动生成基线、脚本缺失/异常/外部请求/超时及构建命令失败被归类为执行错误。自检结束自动清理目录和服务。 + +平台验收及实测耗时见 [验收摘要](./cases/ACCEPTANCE.md)。视觉测试不替代单元测试、性能测试及内网 BugServer 的发版前全量测试。 diff --git a/packages/vchart/__tests__/visual/cases/ACCEPTANCE.md b/packages/vchart/__tests__/visual/cases/ACCEPTANCE.md new file mode 100644 index 0000000000..9c9ae343ab --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/ACCEPTANCE.md @@ -0,0 +1,36 @@ +# 视觉测试验收摘要 + +截至 2026-09-23,256 个本地用例的 macOS 与 Linux 验收通过。Linux 实测 veLinux 2 x64;Ubuntu 未独立实测,不作为本轮交付阻塞项。该结论不宣称所有发行版、浏览器或全部 VChart 功能均已覆盖。 + +## 实测环境与结果 + +两平台均使用 Node 22.22.2、Playwright 1.63.0、Chromium 153.0.8010.12;单 worker、无自动重试、DPR 1、en-US、UTC,颜色阈值 0.1、允许差异像素数 0。同机比较两侧,不要求跨平台像素一致。 + +| 环境 | 结果 | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| macOS arm64 | 五轮 256/256 自比较、官方 develop 256/256、固定 SHA 单例和 api/update 目录通过 | +| veLinux 2 x64(Debian bookworm 系、glibc 2.36) | 五轮 256/256 自比较、官方 develop 独立安装/构建后 256/256、固定 SHA 缓存命中单例和 api/update 目录通过 | + +每个平台的五轮稳定性均为首轮正式 CLI 构建,后四轮复用同一产物并重新生成两侧截图,并非五次重新构建。Linux 上 Playwright 使用 ubuntu24.04-x64 浏览器构建;Arial 回退到 Liberation Sans,中文字体为 Noto Sans CJK SC。 + +两平台均完成 32 项工具检查;15 项静态目标配置变异检出差异,15 项匹配流程对照通过;50 项动作抑制返回执行错误,50 项正常交互基线通过,50 项候选动作省略的截图反例检出差异。反例只运行在独立验证副本中,正式执行不跳过断言;这些结果不是全部 spec 分支的变异覆盖率。 + +同时验证了候选颜色/位置/标签差异、空图、资源与渲染失败、超时、缺图、结果集合、报告及清理失败、离线和搬迁后报告。未提交源码进入本地构建,fork origin 不改变官方基线,损坏缓存重建,锁冲突不影响原任务,构建/截图/基线安装期间中断均留下错误诊断并清理本次资源。Linux 证据取回后已清理远端测试内容。 + +Linux 验收期间修复 Git 自动延长对象缩写导致的工作区摘要误报,改用 `git diff --binary --full-index`。五轮稳定性在该修复前完成;修复不改变构建配方、用例或渲染。修复后重新通过 Linux 完整工具检查、官方全量、单例、目录、未提交源码、缓存及中断验证,macOS 相关回归检查 8/8 通过。 + +## 耗时参考 + +官方基线固定为 `67400f3fb6501f62455392089b7a7d8367cf6b9a`。以下为特定机器的实测,不是耗时承诺;网络、依赖和硬件均会影响耗时。 + +| 环境 / 运行 | 本地构建 | 基线安装 / 编译 | 两侧截图 | 总计 | +| ------------------------------------ | -------- | ---------------- | ----------------- | -------- | +| macOS 全量自比较 | 20.54 s | — | 130.95 + 136.73 s | 289.61 s | +| macOS 官方全量(缓存命中) | 21.47 s | 缓存 0.04 s | 128.80 + 135.10 s | 288.15 s | +| Linux 全量自比较 | 61.41 s | — | 298.59 + 321.27 s | 683.21 s | +| Linux 官方全量(基线构建缓存未命中) | 62.20 s | 142.53 / 64.84 s | 311.11 + 325.39 s | 916.38 s | +| Linux 固定 SHA 单例(缓存命中) | 62.37 s | 缓存 0.05 s | 3.79 + 3.70 s | 72.32 s | + +Linux 冷基线运行之前已获取 Git 对象,不代表全新克隆的网络耗时;全量冷基线的清理/报告分别为 5.91 / 0.122 s。macOS 缓存命中耗时不能当作冷启动耗时。 + +截图、日志和详细验收流水不提交到仓库。运行命令、结果协议及清理方式见 [工具说明](../README.md),具体覆盖限制见 [覆盖清单](./COVERAGE.md)。变更用例或执行链路后,应依据变更范围重新验证,不能用历史验收代替新结果。 diff --git a/packages/vchart/__tests__/visual/cases/COVERAGE.md b/packages/vchart/__tests__/visual/cases/COVERAGE.md new file mode 100644 index 0000000000..c02341c1bf --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/COVERAGE.md @@ -0,0 +1,152 @@ +# 视觉测试覆盖与边界 + +本清单帮助开发者和编码 Agent 选择目录,不参与自动筛选。测试默认 VChart 核心 UMD、Chromium 和确定性最终状态,当前登记 256 个用例。以下覆盖仅指列出的条件,不代表全部功能或所有 spec 参数;准确目的、输入和断言以各模块为准。 + +## 常用图表及配置分支 + +| 方向 | 代表用例 | 实际条件与边界 | +| ---------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| 柱图 | bar-stack、bar-group、bar-three-level-gap、bar-percent、bar-width-limit | 正负堆叠、两/三层分组、百分比、宽度限制;不枚举所有间距组合 | +| 折线 | line-gap、line-missing-link、line-step、line-monotone、line-stack-dashed-legend | 缺失值断开/连接、阶梯/单调曲线、堆叠;line-band-invalid-zero 没有真实缺失数据 | +| 面积 | area-horizontal、area-negative、area-stacked、area-unstacked、area-missing、area-invalid-zero | 横向、正负值、堆叠开关、缺失值 break/zero;保留来源数据 | +| 散点 | scatter-symbol、data-fields-domain、scale-domain-replace | 位置、大小和形状;异常输入过滤;共同比例尺及定义域替换 | +| 饼/环图 | pie-body、pie-negative、pie-nested、pie-radius-scale、pie-empty-placeholder、pie-zero-placeholder、pie-show-all-zero | 负值、嵌套、半径编码、空数组/全零、占位环;单独验证占位图实际绘制 | +| 瀑布 | waterfall、waterfall-leader-line | 累计/总计/连接线;反向类目轴和变化值堆叠标签;不覆盖所有横向和自动总计模式 | +| 雷达/玫瑰 | radar-series、radar-negative、radar-stacked、rose-stack、rose-group | 极坐标、负值、堆叠与分组;不将配置了 hover 视为已测 hover | +| 进度/仪表 | progress-linear-\*、progress-circular-gradient、gauge-gradient、gauge-tick-segment | 渐变、留白、clamp、指针与刻度分段;不覆盖实时动画 | +| 标签与标注 | pie-label、label-stack-zero、multi-bar-inside、`mark-line-*`、`mark-area-*`、mark-point-symbol | 外标签/引导线、零值堆叠、聚合扩轴、坐标标注、标注点文字及格式回调 | +| 常用交互 | legend-filter、tooltip-hover、datazoom-drag、brush-select | 实际选择/提示/范围/刷选状态;抑制动作应失败 | +| 更新 | update-resize、update-state-scatter-area、update-indicator-visible、update-pie-empty | 数据、尺寸、状态、隐藏到显示及更新为空值;检查最终实例和图元 | + +## 其他图表与核心分支 + +| 方向 | 代表用例 | 实际条件与边界 | +| -------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| 区间/分箱/统计 | range-column-horizontal、range-area-horizontal、range-area-missing-bound、histogram-min-height、box-plot-\* | 双边界、末尾缺失上界、最小柱高、五数集合和分组;末尾缺失不等同于区间内部断点 | +| 漏斗 | funnel-directions、funnel-transform | 四区域的上下方向、对齐和外标签;转化形状和转化标签 | +| 层次/关系 | sankey-horizontal、sankey-vertical、treemap-hierarchy、circle-packing-padding、sunburst-gap、correlation-tooltip | 横纵流图、层次矩形/圆/扇形、层间留白、关联图悬停;sunburst-drill、treemap-drill、circle-packing-drill-\* 分别验证下钻及回退 | +| 热力/词云 | heatmap-correlation、word-cloud-enlarge | 原相关矩阵、固定区域、颜色映射;random=false 的词云及放大配置。fontSizeLimitMax 保留源配置但不计为实际字号上限覆盖 | +| 轴 | axis-log、axis-symlog、axis-time-brush、axis-zero-align、axis-tick-align、axis-break、axis-unit-position | log/symlog/time、双轴同步、断轴、单位;时间轴 case 保留刷选配置但不宣称执行了刷选 | +| 轴标签 | axis-label、axis-auto-hide、axis-polar-multiple-labels、axis-multilevel-wrap | 显式旋转、隐藏、多层极坐标、自动换行;非所有字体/截断/断轴组合 | +| 图例/范围组件 | legend-multiple、legend-symbol-hidden、legend-continuous-filter、scrollbar-axis-range、datazoom-preview | 多图例、符号隐藏、连续筛选;隐藏滚动条的初始范围;缩窄页面以实际触发 auto dataZoom | +| 布局/主题 | grid-multiple-axes、grid-line-label-layout、combination-line-pie、theme-stack、bar-title | 多区域、多系列、多轴绑定、专用主题和标题;不等同于所有布局算法分支 | +| Crosshair/自定义图元 | crosshair-polar-default、custom-mark-click | 默认选中的极坐标 crosshair;按 markName 绑定点击并修改目标图元 | + +## spec 分支与组件行为 + +静态检查不只依赖两份截图相同:源配置/数据核对、有效图元检查以及独立的受控变异共同约束覆盖。配置存在、模型建议和模块数量都不是功能覆盖率。 + +| 目录 | 代表用例 / 配置分支 | 实际验证及边界 | +| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `charts/bar` | bar-stack-corner、bar-min-width、bar-group-gap、bar-gradient-\* | 堆叠圆角、最小宽度、组内间距、渐变和标签;不穷举所有带宽组合 | +| `charts/area` | area-stream-center、area-step-curve、area-series-mark、area-point-outer-border | silhouette 居中偏移、阶梯曲线、point 主图元、点外描边;主图元另查实际 mark 名称 | +| `charts/scatter` / `data` | scatter-ordinal-_、scatter-_-callback、scatter-radial-gradient、scale-domain-expand、scale-specified-shape | 离散/回调的形状及大小、径向渐变、共享 scale 定义域扩展、指定形状映射 | +| `charts/box-plot` | box-plot-bar-shaft、box-plot-outliers | 柱形须线及宽度、离群点;API 悬停另见 `api/state` | +| `charts/funnel` | funnel-left/right-align-_、funnel-bottom-align-_、funnel-cone-transform | 左右/下向对齐和长外标签、锥形转化;不是所有方向与样式的笛卡尔积 | +| `charts/progress` | progress-tick-mask、progress-series-track、progress-vertical-padding、progress-threshold-color | 刻度遮罩、系列轨道、竖向及边距、阈值颜色;progress-tick-mask 按源 move 记录验证 hover 留白及 indicator 内容 | +| `charts/sankey` | sankey-node-align、sankey-cross-align-start/middle/end | 主方向及交叉方向布局;邻接选择另由 sankey-selected-adjacency / sankey-adjacency 验证 | +| `charts/waterfall` | waterfall-horizontal-total、waterfall-absolute-label、waterfall-total-tag、waterfall-total-callback、waterfall-summary-total | 横向字段总计、绝对值标签、tag-only/custom product、汇总字段与格式回调;clampForce 的边界触发仍为空缺 | +| `components/axis` | axis-end-symbol、axis-explicit-domain、axis-auto-rotate、axis-vertical-text、axis-visible-layers、axis-trim-padding、axis-unit-offset、axis-polar-title-background、axis-label-callback | 端点、显式域、旋转/竖排/多层、裁剪留白、单位偏移、极轴背景和格式回调;未把旧 band 轴 tickCount 用例计为数值轴刻度覆盖 | +| `components/marker` | marker-end-symbols、marker-_-position、marker-monotone-line、marker-polar-coordinates、marker-_-statistics、marker-label-\*、marker-multi-segment、marker-expand-callback、marker-background-shape | 符号、绝对/相对/极坐标、单调曲线、统计/回调、多段连接和自定义背景;保留各源组合,不宣称所有组合已枚举 | +| `components/richtext` | richtext-axis-marker、richtext-funnel-label、richtext-pie-title、richtext-range-label、richtext-line-end-label、richtext-marker-label、richtext-extension-mark | 检查真实 richtext 图元;富文本轴、标注、图表标签、标题、末端及扩展文本。range 用例 position=middle,不计 minLabel/maxLabel 分支 | +| `components/label` / `charts/treemap` | line-end-label、treemap-nonleaf-label | 末端标签、非叶节点及富文本;不穷举标签测量与避让策略 | +| `components/indicator` / `components/crosshair` | indicator-autofit、crosshair-dual-axis-default | 静态内容自适应、双轴默认选中;不把未执行的 hover 联动计覆盖 | +| `components/datazoom` | datazoom-vertical-initial-range | 纵向初始窗口及轴布局;保留源 maxSpan,但未验证拖动上限 | +| `components/legend` / `components/scrollbar` | legend-clear-selection、legend-single-selection、scrollbar-drag-\*、scrollbar-range-mode | 图例全清后恢复、单选过滤,横/纵向拖动真实改变范围及图元位置,percent/value 混合范围。空图状态只断言,最终截图为恢复后的有效图 | +| `components/tooltip` | tooltip-click、tooltip-trigger-off-none、tooltip-canvas-click/hover、tooltip-disabled、tooltip-title-hidden、tooltip-position-\* | 实际点击/悬停、HTML/Canvas 可见内容、关闭/不关闭、隐藏标题及五种 mark 定位;不含外部图片和动画过渡 | +| `interaction` | area-selected-state、combination-hover-reverse、line-hover/selected-reverse、radar-selected-reverse、rose-selected-state、treemap-selection-persist、treemap-label-state、sunburst-drill | 点/扇区状态和反向状态、triggerOff=none 保留选择、标签状态、旭日图实际下钻;不声称重放来源全部历史坐标或所有子例 | +| `api/update` / `api/state` | update-axis-sampling、update-legend-data、box-plot-api-hover | updateSpecSync 切换 sampling、先选择图例再 updateDataSync、setHovered 的真实最终状态 | + +按 `--dir components/richtext`、`--dir interaction`、`--dir api` 或 `--case sunburst-drill` 选择。完整选择以 `index.mjs` 和 `--list` 为准。 + +## 交互、布局及 API 的具体用例 + +| 目录 / 用例 | 验证目的 | +| -------------------------------------------------------------------------------------- | ------------------------------------------------ | +| [axis-force-tick-count](./components/axis/axis-force-tick-count.mjs) | 多个数值轴的 tickCount 与 forceTickCount 组合 | +| [range-area-vertical](./charts/range-area/range-area-vertical.mjs) | 纵向区间面积的上下界及半透明填充 | +| [range-area-average-line](./charts/range-area/range-area-average-line.mjs) | 独立数据源的区间面积与平均值折线组合 | +| [axis-inside-four-sides](./components/axis/axis-inside-four-sides.mjs) | 四方向轴的内侧刻度、标签格式与轴线组合 | +| [marker-extension-quadrants](./components/marker/marker-extension-quadrants.mjs) | 散点象限标区与依赖 region 布局的扩展图元 | +| [marker-richtext-position](./components/richtext/marker-richtext-position.mjs) | 起止点富文本标注的尺寸与偏移 | +| [axis-time-layer-step](./components/axis/axis-time-layer-step.mjs) | 时间轴 layers 的 tickStep 与混合范围数据 | +| [axis-tick-count-callback](./components/axis/axis-tick-count-callback.mjs) | 依据轴长和字体计算数值轴刻度数量 | +| [marker-richtext-content](./components/richtext/marker-richtext-content.mjs) | 富文本标注的行内样式与独立末端内容 | +| [marker-area-richtext](./components/richtext/marker-area-richtext.mjs) | 标区内部富文本标签的排版 | +| [marker-coordinate-callbacks](./components/marker/marker-coordinate-callbacks.mjs) | 依据轴域和数据计算线、区、点坐标 | +| [axis-region-overlap](./components/axis/axis-region-overlap.mjs) | 关联不同系列的 region-relative-overlap 多轴布局 | +| [bar-stack-sort-min-height](./charts/bar/bar-stack-sort-min-height.mjs) | 多数据源堆叠排序、逆序与最小柱高 | +| [axis-sync-scrollbar](./components/axis/axis-sync-scrollbar.mjs) | 横向柱图的双数值轴刻度同步与滚动条 | +| [pie-label-custom-path](./components/label/pie-label-custom-path.mjs) | 外标签引导线自定义折角路径 | +| [area-mark-hover](./interaction/area-mark-hover.mjs) | 面积图点与面积分别悬停的真实状态 | +| [tooltip-content-reduce](./components/tooltip/tooltip-content-reduce.mjs) | Tooltip 配置、实际提示内容及移出隐藏 | +| [tooltip-dimension-style](./components/tooltip/tooltip-dimension-style.mjs) | Tooltip 配置、实际提示内容及移出隐藏 | +| [axis-richtext-tooltip](./components/tooltip/axis-richtext-tooltip.mjs) | Tooltip 配置、实际提示内容及移出隐藏 | +| [tooltip-series-dimension](./components/tooltip/tooltip-series-dimension.mjs) | Tooltip 配置、实际提示内容及移出隐藏 | +| [axis-poptip-flush](./components/axis/axis-poptip-flush.mjs) | 截断轴标签悬停后显示完整文本(源录制 poptip) | +| [axis-poptip-rotated](./components/axis/axis-poptip-rotated.mjs) | 截断轴标签悬停后显示完整文本(源录制 poptip) | +| [legend-hover-state](./components/legend/legend-hover-state.mjs) | 图例悬停反向状态、移出恢复与单选筛选 | +| [legend-hover-no-filter](./components/legend/legend-hover-no-filter.mjs) | 图例悬停反向状态、移出恢复 | +| [hover-series-api](./interaction/hover-series-api.mjs) | 源 pointerover 回调按 Age 更新同组及反向状态 | +| [update-full-data-marker](./api/update/update-full-data-marker.mjs) | updateFullDataSync 原始更新值与标记组件 | +| [pie-update-zero](./api/update/pie-update-zero.mjs) | 全零饼图经原始 updateData 更新为有效扇区 | +| [dimension-index-area](./api/state/dimension-index-area.mjs) | setDimensionIndex 定位指定维度的十字线 | +| [custom-mark-text-click](./interaction/custom-mark-text-click.mjs) | customMark 点击回调修改实际图元 | +| [custom-mark-fill-click](./interaction/custom-mark-fill-click.mjs) | customMark 点击回调修改实际图元 | +| [datazoom-min-max-span](./components/datazoom/datazoom-min-max-span.mjs) | DataZoom 拖动及有效范围约束 | +| [datazoom-vertical-fixed-span](./components/datazoom/datazoom-vertical-fixed-span.mjs) | DataZoom 拖动及有效范围约束 | +| [datazoom-time-span](./components/datazoom/datazoom-time-span.mjs) | DataZoom 拖动及有效范围约束 | +| [datazoom-preview-brush](./components/datazoom/datazoom-preview-brush.mjs) | DataZoom 拖动及有效范围约束 | +| [scrollbar-auto](./components/scrollbar/scrollbar-auto.mjs) | 滚动条拖动与实际可视范围 | +| [scrollbar-crosshair](./components/scrollbar/scrollbar-crosshair.mjs) | 滚动条拖动与实际可视范围 | +| [scrollbar-axis-click-update](./components/scrollbar/scrollbar-axis-click-update.mjs) | 滚动条拖动后点击轴标签更新柱颜色 | +| [sankey-selected-adjacency](./charts/sankey/sankey-selected-adjacency.mjs) | 点击桑基节点时仅高亮相邻节点与连接,空白点击恢复 | +| [sankey-adjacency](./charts/sankey/sankey-adjacency.mjs) | 点击桑基节点时仅高亮相邻节点与连接,空白点击恢复 | +| [sankey-node-state](./charts/sankey/sankey-node-state.mjs) | 桑基节点悬停与点击状态 | +| [circle-packing-drill-rooted](./charts/circle-packing/circle-packing-drill-rooted.mjs) | 层级图下钻与空白回退的路径和实际布局 | +| [circle-packing-drill-forest](./charts/circle-packing/circle-packing-drill-forest.mjs) | 层级图下钻与空白回退的路径和实际布局 | +| [circle-packing-drill-event](./charts/circle-packing/circle-packing-drill-event.mjs) | 层级图下钻与空白回退的路径和实际布局 | +| [treemap-drill](./charts/treemap/treemap-drill.mjs) | 层级图下钻与空白回退的路径和实际布局 | +| [brush-polygon](./interaction/brush-polygon.mjs) | 多边形刷选的实际命中及排除状态 | +| [brush-region-link](./interaction/brush-region-link.mjs) | 跨区域刷选联动的实际命中及排除状态 | +| [indicator-richtext-click](./components/indicator/indicator-richtext-click.mjs) | 饼图选择触发富文本指标及对应数值 | +| [pie-label-hover](./charts/pie/pie-label-hover.mjs) | 半圆饼图悬停状态及标签引导线布局 | +| [funnel-outer-label-line](./charts/funnel/funnel-outer-label-line.mjs) | 漏斗外标签对齐、虚线与悬停 | +| [legend-custom-value](./components/legend/legend-custom-value.mjs) | 自定义图例值与点击后的真实数据筛选 | +| [crosshair-polar-formatters](./components/crosshair/crosshair-polar-formatters.mjs) | 极坐标十字线分类和数值 formatter 的真实悬停输出 | +| [theme-runtime-switch](./theme/theme-runtime-switch.mjs) | 主题注册和实例切换后的圆角与配色 | +| [theme-chart-components](./theme/theme-chart-components.mjs) | 图表主题启用柱标签并定位左侧图例 | +| [funnel-extension-select](./charts/funnel/funnel-extension-select.mjs) | 漏斗扩展图元、初始选择和后续点击选择切换 | +| [media-query-width](./layout/media-query-width.mjs) | 媒体查询注册、缩窄后隐藏左侧坐标轴标签 | + +## 必要触发条件与限制 + +- 来源宿主尺寸不全的鼠标录制,改为实际图元定位;不宣称逐像素重放历史坐标,也不宣称复现原缺陷。 +- 截断标签 Poptip:一例由 350×200 缩窄到 180×200 后悬停;另一例选择避开轴交点的实际截断标签。 +- 自动滚动条:九个类别在 800px 下不溢出,缩窄到 360px 后验证滚动。带 crosshair 的来源当前未把拖动后的十字线行为计为覆盖。 +- 缩放:保留来源 minSpan/maxSpan、固定跨度和预览刷选配置,核验实际范围及图元变化;预览刷选保留 throttle,等待范围事件到达终点后再松开。不是所有上下限/滚轮/触控组合均已测试。 +- 跨区域刷选:在三份原始数据共同拥有的 x=5 上刷选;对每个关联系列检查命中与未命中。原第三份数据从 x=5 开始,不能要求它命中 x=0。 +- updateSpec:先保存实际滚动中间态,再核验轴标签点击后的原始配置更新及红色柱;更新恢复初始范围是来源代码行为。 +- 媒体查询:注册插件后重建实例,再缩窄到 160×160,验证 maxWidth 条件隐藏左轴。探索性增宽未观察到标签恢复,保留为待调查路径,不纳入通过结论。 +- 主题:保留“注册并激活后创建实例”与“实例切换主题”两种来源顺序。 +- 无鼠标录制但带 drill:true 的来源增加实际下钻检查,不称为录制重放。下钻/回退均核验事件路径与布局变化。 +- 范围面积图的双来源合并只覆盖共同静态条件;strokeWidth/lineWidth 的 hover 差异仍未覆盖。 + +### 图元状态与多步交互 + +点/线/面积按真实图元分别检查 hover、selected 及反向状态;面积图没有独立 line 图元时,不把 line 配置出现当作另一条已测路径。Canvas Tooltip 检查两个 datum、隐藏和恢复;刷选检查创建、空白清除、重新创建;旭日图检查下钻/回退/另一分支;矩形树图检查切换选择后空白点击仍保留。既有图例清空并恢复实现保留,仍参加反例验证。 + +## 未覆盖与范围外场景 + +- 地图/Geo:尚未准备地理资源、投影及地理标注专项用例。 +- sequence:尚无本地有效覆盖,需独立准备公开、确定性的时间关系用例。 +- liquid、venn、mosaic:不在当前默认 UMD 注册范围,需要先明确注册契约。 +- 动画过程、3D、外部图片/字体、随机布局、扩展包、多产品及其他浏览器不在当前范围。 +- Player 播放、富文本完整排版分支、更多事件协作及滚轮/触控手势未全面覆盖;配置出现不代表行为已执行。 +- rangeArea 区间内部缺失上下界、rangeColumn 富文本 minLabel/maxLabel 端点分支仍未覆盖;现有末尾缺值与 middle 标签不能替代。 +- 媒体查询增宽后的标签恢复未纳入通过结论;仅覆盖当前缩窄触发。 +- 旧 dimension 状态缺少有效触发、无效分类值的 setDimensionIndex 未产生目标效果,不用普通 hover 或有效维度用例冒充覆盖。 +- 瀑布 stackLabel.clampForce 尚无有效越界触发的验证;保留该字段的用例不算覆盖其截断行为。 +- ManualTicker、manual 模式、autoRender:false 和手动 tick 时序需要独立宿主契约。 + +开发者可以依据新功能或缺陷独立补充有效用例,遵循 [编写与来源约定](./README.md)。未迁移的 BugServer 用例不能据此判为冗余或删除。平台状态见 [验收摘要](./ACCEPTANCE.md)。 diff --git a/packages/vchart/__tests__/visual/cases/README.md b/packages/vchart/__tests__/visual/cases/README.md new file mode 100644 index 0000000000..1ebf7142d7 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/README.md @@ -0,0 +1,61 @@ +# 本地视觉测试用例 + +一份 case 一个文件,按主要验证目的归类,`index.mjs` 是唯一可执行清单。开发者或编码 Agent 根据改动选择测试范围;工具不推断源码影响范围。当前已登记 256 个用例,具体 ID、目的和文件用 `--list` 查询,配置分支与限制见 [覆盖清单](./COVERAGE.md)。 + +## 目录与选择 + +| 目录 | 主要目的 | 选择示例 | +| ----------------------------- | -------------------------------------- | ------------------------ | +| `charts/` | 图表主体、类型特有配置和数据映射 | `--dir charts/waterfall` | +| `components/` | 轴、标签、图例、标注、提示、缩放等组件 | `--dir components/label` | +| `data` | 数据字段、过滤与公共比例尺 | `--dir data` | +| `layout` | 多区域布局、媒体查询等布局条件 | `--dir layout` | +| `theme` | 主题配置、注册和切换 | `--dir theme` | +| `interaction` | 图元状态、事件及跨组件协作 | `--dir interaction` | +| `api`(含 `update`、`state`) | 数据、spec、尺寸与状态 API | `--dir api` | + +组件自身的交互仍放组件目录,例如图例筛选在 `components/legend`;不要按是否有鼠标动作重复存放。饼图标签在 `components/label`,只测 `charts/pie` 不会包含它。修改共享模块时应考虑多个目录,不确定范围时运行全量。 + +从仓库根目录执行: + +```sh +node packages/vchart/scripts/visual-test.mjs --list +node packages/vchart/scripts/visual-test.mjs --dir components/label +node packages/vchart/scripts/visual-test.mjs --case pie-label +node packages/vchart/scripts/visual-test.mjs +``` + +目录相对于本目录,递归包含子目录;`--dir` 与 `--case` 互斥。默认比较官方 develop,运行及报告说明见 [工具 README](../README.md)。 + +## 新增或维护用例 + +1. 先查看清单、覆盖说明和相近模块,确定新增的验证目的;新功能、缺陷修复及未覆盖的有效配置均可新增回归用例。已有用例能清晰表达时优先补强,避免一个用例承载互不相关的目的。 +2. 在对应目录创建 `.mjs`,在 `index.mjs` 登记唯一 `id`、一句话 `purpose`、`./目录/文件.mjs`。`sourceExample` 只填写真实公开示例的仓库相对路径,无来源则省略;未经登记的文件不会执行。 +3. 实现 `createSpec()`、可选 `exercise(page)` 和必需 `verify(page)`。固定数据,每次创建新 spec;不导入当前 VChart 源码,不读取网络、Node API 或测试框架运行时代码。 +4. 交互动作与最终断言分开。检查实际筛选数据、提示内容、状态、范围或尺寸,不以点击成功、固定等待或截图存在代替验证。复用 `helpers.mjs`、`interaction-helpers.mjs` 中已有定位与等待函数。 +5. 运行 `--list`、`--check`,再运行 `--self-compare --case ` 及默认基线单例比较。新用例在相同构建下连续五轮自比较稳定,并用针对验证目的的受控变化证明它能发现问题;交互还需验证抑制动作时不能通过。故障注入仅在独立副本操作,不提交破坏性修改。 +6. 开发完成后运行相关目录;提交 PR 前运行默认全量比较,检查三图或 Agent 摘要并说明差异。无法运行或基线不支持新功能时如实记录阻断,不跳过用例或将自比较当作基线通过。 + +静态用例检查类型、关键数据、配置和必要图元,公共检查确认有效绘制,截图负责布局差异。配置回调或确定性计算可复用 `verifySourceSpec`,普通配置可用 `verifySpec`;输入一致仍不代表目标效果成立。复用共享检查,不再编写一套布局算法。 + +每个模块顶部说明目的、关键条件和边界,核心函数补中文注释。独立新增模块示例(不含 BugServer ID): + +```js +/** + * 验证目的:说明本例要防止的具体回归。 + * 图表类型:bar。 + * 关键配置:列出实际验证的 spec 字段。 + * 场景条件:说明固定数据关系和触发条件。 + * 最终检查:说明语义断言和截图分别验证什么。 + * 覆盖边界:明确未执行的动作或未覆盖的组合。 + */ +``` + +完整模块示例见 [工具 README](../README.md#用例约定)。不得用放宽截图阈值、跳过断言或永久接受差异替代修复确定性问题。 + +## BugServer 来源约定 + +- 现有迁移项在模块头唯一维护 `BugServer case IDs: , `,改写保留原 ID,合并保留全部真实来源 ID。保留必要触发条件、宿主适配和覆盖边界说明,勿带入原始业务数据或内部链接。 +- 后续开发者或 Agent 独立新增的用例不填写此行;不写空值、TODO、占位或虚构 ID,也不为通过检查伪造 `sourceExample`。来源注释不是执行前提。 +- 后续计划将独立新增用例同步到 BugServer,确认同步成功后才补齐服务返回的真实 ID。本期不实现同步命令、任务或状态字段;缺少 ID 不影响本地测试。 +- 已有 ID 不应因重命名、移动或改写用例丢失。ID 只用于追溯,不代表已与线上内容实时同步;测试无需访问 BugServer。 diff --git a/packages/vchart/__tests__/visual/cases/api/state/box-plot-api-hover.mjs b/packages/vchart/__tests__/visual/cases/api/state/box-plot-api-hover.mjs new file mode 100644 index 0000000000..0b039078b4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/state/box-plot-api-hover.mjs @@ -0,0 +1,189 @@ +import { verifySpec, verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 692574968b99420069ae807f + * 验证目的:分组箱线图公开 API 设置悬停状态。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const data = [ + { + name: 'boxPlot', + values: [ + { + x: 'Sub-Saharan Africa', + y1: 9.16270124, + y2: 10.07530816, + y3: 10.09027907, + y4: 10.27579542, + y5: 11.62222959, + group: 'High income' + }, + { + x: 'Sub-Saharan Africa', + y1: 8.721525214, + y2: 9.641352839, + y3: 10.1736233, + y4: 10.57169914, + y5: 11.64427467, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.404757278, + y2: 10.36482449, + y3: 10.94903493, + y4: 11.5806383, + y5: 12.50192084, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.732841997, + y2: 9.732841997, + y3: 9.732841997, + y4: 9.732841997, + y5: 9.732841997, + group: 'High income' + }, + { + x: 'Middle East & North Africa', + y1: 9.541951901, + y2: 10.33719892, + y3: 10.91206173, + y4: 11.29821858, + y5: 11.60653481, + group: 'Low income' + }, + { + x: 'Middle East & North Africa', + y1: 10.2396509, + y2: 10.63879995, + y3: 11.09996104, + y4: 11.54301107, + y5: 11.92092709, + group: 'High income' + }, + { + x: 'Latin America & Caribbean', + y1: 10.14653181, + y2: 10.32106777, + y3: 10.45467215, + y4: 10.45844052, + y5: 10.6064696, + group: 'Low income' + }, + { + x: 'Latin America & Caribbean', + y1: 8.743652009, + y2: 9.413881158, + y3: 10.16606248, + y4: 11.00011805, + y5: 12.20655104, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 7.800035977, + y2: 8.850646235, + y3: 10.14633178, + y4: 11.59877618, + y5: 13.24880824, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 8.316035904, + y2: 9.038602613, + y3: 10.22954548, + y4: 10.71782871, + y5: 12.07411874, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.831631935, + y2: 9.939275167, + y3: 10.39108655, + y4: 10.95556656, + y5: 11.3012157, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.522480948, + y2: 10.43085982, + y3: 11.06642694, + y4: 11.73822523, + y5: 12.62940296, + group: 'High income' + } + ] + } + ]; + const spec = { + type: 'boxPlot', + data: data, + xField: ['x', 'group'], + minField: 'y1', + q1Field: 'y2', + medianField: 'y3', + q3Field: 'y4', + maxField: 'y5', + seriesField: 'group', + direction: 'vertical', + color: ['#62CDFF', '#9E4784'], + legends: { + visible: true + }, + title: { + visible: true, + text: 'Global GDP 2021' + }, + boxGapInGroup: 5, + outlier: { + style: { + stroke: '#ccc', + lineWidth: 2, + fill: 'transparent' + } + }, + boxPlot: { + style: { + stroke: '#ccc', + boxWidth: '100%', + shaftWidth: '50%', + shaftShape: 'filled-line', + lineWidth: 1 + }, + state: { + hover: { + lineWidth: 2 + } + } + } + }; + + return spec; + }, + async exercise(page) { + // 保留源操作类型,并以真实实例或图元定位执行。 + await page.evaluate(() => { + const c = window.__visualChart; + c.setHovered(c.getSpec().data[0].values[0]); + }); + }, + async verify(page) { + // 验证目标状态及实际绘制,动作缺失不能通过。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.waitForFunction( + () => + window.__visualChart + .getStage() + .findAll(g => g.currentStates?.includes('hover') && g.attribute.lineWidth === 2, true).length > 0 + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/state/dimension-index-area.mjs b/packages/vchart/__tests__/visual/cases/api/state/dimension-index-area.mjs new file mode 100644 index 0000000000..98f7433c9f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/state/dimension-index-area.mjs @@ -0,0 +1,125 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 677c9b8eec437f00b3c5121a + * 验证目的:setDimensionIndex 定位来源指定维度的十字线。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:验证来源 setDimensionIndex 之后的实际十字线;附带 move 录制不在本例的验证范围。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'area', + data: { + values: [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } + ] + }, + title: { visible: true, text: '100% stacked area chart of cosmetic products sales' }, + percent: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [{ visible: true, position: 'middle', orient: 'bottom' }], + axes: [ + { + orient: 'left', + label: { + formatMethod(val) { + return `${(val * 100).toFixed(2)}%`; + } + } + } + ], + area: { + style: { + fill: { + gradient: 'linear', + x0: 0.5, + y0: 0, + x1: 0.5, + y1: 1, + stops: [ + { + offset: 0, + opacity: 1 + }, + { + offset: 1, + opacity: 0 + } + ] + } + } + }, + tooltip: { + offset: { x: 50, y: 50 } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.setDimensionIndex('Nail polish'); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(value => { + const c = window.__visualChart; + const graphics = c + .getStage() + .findAll(g => g.name?.toLowerCase().includes('crosshair') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('指定维度没有显示 crosshair'); + const text = c + .getStage() + .findAll( + g => g.type === 'text' && String(g.attribute.text).includes(value) && g.attribute.visible !== false, + true + ); + if (!text.length) throw Error('指定维度文本不存在'); + }, 'Nail polish'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update-indicator-visible.mjs b/packages/vchart/__tests__/visual/cases/api/update-indicator-visible.mjs new file mode 100644 index 0000000000..02e05ec79b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update-indicator-visible.mjs @@ -0,0 +1,122 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 6795db96d2dacd00b3443edb + * 验证目的:updateSpecSync 后显示仪表指标文字。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:保留来源动作或按源事件执行操作并检查结果。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const spec = { + type: 'pie', + color: ['#4E83FD', '#50CEFB', '#935AF6', '#FAD355', '#F76964', '#FFA53D'], + data: { + values: [ + { + channel_type: 'Marketplace', + amount: 262 + }, + { + channel_type: 'Website', + amount: 282 + }, + { + channel_type: 'Mini app', + amount: 311 + }, + { + channel_type: 'Store', + amount: 350 + }, + { + channel_type: 'Phone', + amount: 400 + } + ] + }, + label: { + visible: true + }, + tooltip: { + mark: { + content: {} + }, + dimension: { + content: {} + } + }, + legends: { + visible: false, + orient: 'bottom', + position: 'middle' + }, + indicator: { + visible: false, + title: { + visible: true, + style: { + text: 'bbb' + } + }, + content: [ + { + visible: true, + style: { + fontSize: 20, + text: '2222' + } + } + ] + }, + valueField: ['amount'], + categoryField: ['channel_type'], + innerRadius: 0.55 + }; + + return spec; + }, + async exercise(page) { + // 适配源动作;鼠标定位按实际图元,避免绑定旧宿主像素坐标。 + await page.evaluate(() => { + const chart = window.__visualChart; + const indicator = chart.getSpec().indicator; + const initial = Array.isArray(indicator) ? indicator[0] : indicator; + if ( + initial.visible !== false || + chart + .getStage() + .findAll( + g => + g.type === 'text' && g.attribute.visible !== false && ['bbb', '2222'].includes(String(g.attribute.text)), + true + ).length + ) + throw new Error('indicator 初始隐藏条件不满足'); + window.__indicatorInitiallyHidden = true; + }); + await page.evaluate(async () => { + const chart = window.__visualChart; + const { cases, loadCase } = await import('/suite/cases/index.mjs'); + const spec = ( + await loadCase(cases.find(c => c.id === new URL(location.href).searchParams.get('case'))) + ).createSpec(); + chart.updateSpecSync({ ...spec, indicator: { ...spec.indicator, visible: true } }); + }); + }, + async verify(page) { + // 完整配置和回调由 verifySpec 检查,另验证最终绘制或操作结果。 + await verifySpec(page, { ...this.createSpec(), indicator: { ...this.createSpec().indicator, visible: true } }); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + if (!window.__indicatorInitiallyHidden) throw new Error('缺少 indicator 隐藏到显示的状态转换'); + const texts = window.__visualChart + .getStage() + .findAll(g => g.type === 'text' && g.attribute.visible !== false, true) + .map(g => String(g.attribute.text)); + if (!texts.includes('bbb') || !texts.includes('2222')) throw new Error('indicator 更新未显示'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update-pie-empty.mjs b/packages/vchart/__tests__/visual/cases/api/update-pie-empty.mjs new file mode 100644 index 0000000000..4ac4cb03f0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update-pie-empty.mjs @@ -0,0 +1,78 @@ +import { verifySpec, verifyEmptyPie } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 67ed193db17ad700a7818601 + * 验证目的:更新饼图数据为 null 和零后切换占位图。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:保留来源动作或按源事件执行操作并检查结果。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'oxygen', value: 10 }, + { type: 'silicon', value: 30 }, + { type: 'aluminum', value: 50 } + ] + } + ], + // supportNegative: true, + outerRadius: 0.8, + innerRadius: 0.5, + padAngle: 0.6, + valueField: 'value', + categoryField: 'type', + pie: { + style: { + cornerRadius: 10 + }, + state: { + hover: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + }, + selected: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + } + } + }, + legends: {}, + label: { + visible: true + }, + emptyPlaceholder: { + showEmptyCircle: true + } + }; + + return spec; + }, + async exercise(page) { + // 适配源动作;鼠标定位按实际图元,避免绑定旧宿主像素坐标。 + await page.evaluate(() => + window.__visualChart.updateData('id0', [ + { type: 'oxygen', value: null }, + { type: 'silicon', value: null }, + { type: 'aluminum', value: 0 } + ]) + ); + }, + async verify(page) { + // 完整配置和回调由 verifySpec 检查,另验证最终绘制或操作结果。 + await verifySpec(page, { type: 'pie', emptyPlaceholder: this.createSpec().emptyPlaceholder }); + await verifyEmptyPie(page); + await page.evaluate(() => { + const data = window.__visualChart.getChart().getAllSeries()[0].getRawData().latestData; + if (data.length !== 3 || data[0].value !== null || data[1].value !== null || data[2].value !== 0) + throw new Error('空值更新未生效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update-resize.mjs b/packages/vchart/__tests__/visual/cases/api/update-resize.mjs new file mode 100644 index 0000000000..1b59831b1d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update-resize.mjs @@ -0,0 +1,40 @@ +import { barSpec } from '../../helpers.mjs'; + +/** + * 验证目的:数据更新与尺寸调整(event-update-spec)。 + * 图表类型:bar。 + * 关键配置:updateData、resize。 + * 场景条件:固定初始两组数据,更新为两条数据并调整为 640×480。 + * 最终检查:验证实际数据含 Updated=55、条数为二、Canvas 尺寸正确。 + * 覆盖边界:不验证 updateSpec 或连续多次更新。 + */ +export default { + createSpec: barSpec, + async exercise(page) { + // 等待公开异步 API 完成,并检查实际数据和画布尺寸。 + await page.evaluate(async () => { + await window.__visualChart.updateData('data', [ + { x: 'Updated', y: 55, group: 'Alpha' }, + { x: 'Second', y: 20, group: 'Alpha' } + ]); + const container = document.getElementById('chart'); + container.style.width = '640px'; + container.style.height = '480px'; + await window.__visualChart.resize(640, 480); + }); + }, + async verify(page) { + // 检查更新后的数据与真实画布尺寸。 + await page.evaluate(() => { + const canvas = window.__visualChart.getCanvas(); + const data = window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData; + if ( + canvas.width !== 640 || + canvas.height !== 480 || + data.length !== 2 || + !data.some(item => item.x === 'Updated' && item.y === 55) + ) + throw new Error('更新或 resize 未生效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update-state-scatter-area.mjs b/packages/vchart/__tests__/visual/cases/api/update-state-scatter-area.mjs new file mode 100644 index 0000000000..e51bc8dd73 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update-state-scatter-area.mjs @@ -0,0 +1,164 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03c1 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:散点与面积图分别按数据过滤更新状态。 + * 保留条件:双系列、面积连续零值、两次 updateState 及 datumKeys/level。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:包含原始状态更新动作;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'common', + series: [ + { + type: 'scatter', + data: { + id: 'data1', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 10 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + point: { + state: { + ddd1: { + size: 30 + }, + hover: { + fill: 'red' + } + } + } + }, + { + type: 'area', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 70 + }, + { + x: 2, + y: 0 + }, + { + x: 3, + y: 0 + }, + { + x: 4, + y: 10 + } + ] + }, + xField: 'x', + yField: 'y', + area: { + state: { + ddd2: { + fill: 'blue' + }, + hover: { + fill: 'red' + } + } + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom', + type: 'band' + } + ] + }; + }, + async exercise(page) { + // 按来源顺序调用状态更新;最终实际图元由 verify 检查。 + await page.evaluate( + actions => { + for (const action of actions) window.__visualChart.updateState(...action.args); + }, + [ + { + method: 'updateState', + args: [ + { + ddd1: { + filter: { datumKeys: ['x', 'y'], datums: [{ x: 1, y: 80 }] }, + level: 6 + } + } + ] + }, + { + method: 'updateState', + args: [ + { + ddd2: { + filter: { datumKeys: ['x', 'y'], datums: [{ x: 1, y: 70 }] }, + level: 6 + } + } + ] + } + ] + ); + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const series = window.__visualChart.getChart().getAllSeries(); + if (series.length !== 2 || series[0].type !== 'scatter' || series[1].type !== 'area') + throw new Error('原始散点与面积系列关联改变'); + const points = series[0].getSeriesMark().getGraphics(); + const selected = points.filter(g => g.currentStates?.includes('ddd1')); + if ( + selected.length !== 1 || + selected[0].context.data[0].x !== 1 || + selected[0].context.data[0].y !== 80 || + selected[0].attribute.size !== 30 + ) + throw new Error('ddd1 未精确命中 x=1、y=80 的散点'); + if (points.some(g => g !== selected[0] && g.attribute.size === 30)) throw new Error('ddd1 错误作用于其他散点'); + // 只要求包含目标数据的面积图元取得状态,不约束图元数量或数据排列。 + const areas = series[1].getSeriesMark().getGraphics(); + if ( + !areas.some( + g => + g.context.data.some(d => d.x === 1 && d.y === 70) && + g.currentStates?.includes('ddd2') && + g.attribute.fill === 'blue' + ) + ) + throw new Error('ddd2 未作用于包含目标数据的面积图元'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update/pie-update-zero.mjs b/packages/vchart/__tests__/visual/cases/api/update/pie-update-zero.mjs new file mode 100644 index 0000000000..dcc2b33bfa --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/pie-update-zero.mjs @@ -0,0 +1,75 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 67ed18f4d3963500b3ec8262 + * 验证目的:全零饼图经原始 updateData 更新为有效扇区。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:执行来源公开更新 API 并核验实际数据与图元,宿主截图钩子由测试流程替代。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'oxygen', value: 0 }, + { type: 'silicon', value: 0 }, + { type: 'aluminum', value: 0 } + ] + } + ], + outerRadius: 0.8, + innerRadius: 0.5, + padAngle: 0.6, + valueField: 'value', + categoryField: 'type', + pie: { + style: { cornerRadius: 10 }, + state: { + hover: { outerRadius: 0.85, stroke: '#000', lineWidth: 1 }, + selected: { outerRadius: 0.85, stroke: '#000', lineWidth: 1 } + } + }, + legends: {}, + label: { visible: true }, + emptyPlaceholder: { showEmptyCircle: true } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await verifySourceSpec(page); + await page.evaluate(() => { + const chartSpace = window.__visualChart; + window.__zeroBefore = chartSpace + .getChart() + .getAllSeries()[0] + .getViewData() + .latestData.every(d => d.value === 0); + chartSpace.updateData('id0', [ + { type: 'oxygen', value: 10 }, + { type: 'silicon', value: 30 }, + { type: 'aluminum', value: 50 } + ]); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifyRendered(page); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + if ( + !window.__zeroBefore || + JSON.stringify(s.getViewData().latestData.map(d => d.value)) !== '[10,30,50]' || + s.getSeriesMark().getGraphics().length !== 3 + ) + throw Error('全零饼图未更新为三个扇区'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update/update-axis-sampling.mjs b/packages/vchart/__tests__/visual/cases/api/update/update-axis-sampling.mjs new file mode 100644 index 0000000000..5a1d9d7607 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/update-axis-sampling.mjs @@ -0,0 +1,104 @@ +import { verifySpec, verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 685e3725e565f800a58eb3ed + * 验证目的:更新轴 sampling 开关后保留旋转标签。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const spec = { + type: 'bar', + data: [ + { + name: 'bar', + fields: { + y: { + alias: 'Sales count' + } + }, + values: [ + { + x: '2021-12-21 2:00', + y: 82 + }, + { + x: '2021-12-21 4:00', + y: 50 + }, + { + x: '2021-12-21 6:00', + y: 64 + }, + { + x: '2021-12-21 8:00', + y: 30 + }, + { + x: '2021-12-21 10:00', + y: 40 + }, + { + x: '2021-12-21 12:00', + y: 40 + }, + { + x: '2021-12-21 14:00', + y: 56 + }, + { + x: '2021-12-21 16:00', + y: 40 + }, + { + x: '2021-12-21 18:00', + y: 64 + }, + { + x: '2021-12-21 20:00', + y: 74 + }, + { + x: '2021-12-21 22:00', + y: 98 + } + ] + } + ], + xField: 'x', + yField: 'y', + axes: [ + { + orient: 'bottom', + sampling: true, + label: { + autoHide: false, + autoRotate: true + } + } + ] + }; + + return spec; + }, + async exercise(page) { + // 保留源操作类型,并以真实实例或图元定位执行。 + await page.evaluate(() => { + const c = window.__visualChart; + const spec = c.getSpec(); + if (spec.axes[0].sampling !== true) throw Error('初始 sampling 未开启'); + c.updateSpecSync({ + ...spec, + axes: [{ orient: 'bottom', sampling: false, label: { autoHide: false, autoRotate: true } }] + }); + }); + }, + async verify(page) { + // 验证目标状态及实际绘制,动作缺失不能通过。 + const expected = this.createSpec(); + expected.axes = [{ orient: 'bottom', sampling: false, label: { autoHide: false, autoRotate: true } }]; + await verifySpec(page, expected); + await verifyRendered(page, 'axis'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update/update-full-data-marker.mjs b/packages/vchart/__tests__/visual/cases/api/update/update-full-data-marker.mjs new file mode 100644 index 0000000000..d88c73ea7b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/update-full-data-marker.mjs @@ -0,0 +1,123 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 64fefdf896294da1e9b882c3 + * 验证目的:updateFullDataSync 原始更新值与标记组件。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:执行来源公开更新 API 并核验实际数据与图元,宿主截图钩子由测试流程替代。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'line', + data: { + id: 'line', + values: [ + { time: '2:00', value: 8 }, + { time: '4:00', value: 9 }, + { time: '6:00', value: 11 }, + { time: '8:00', value: 14 }, + { time: '10:00', value: 16 }, + { time: '12:00', value: 17 }, + { time: '14:00', value: 17 }, + { time: '16:00', value: 16 }, + { time: '18:00', value: 15 } + ] + }, + xField: 'time', + yField: 'value', + markLine: [ + { + y: data => { + console.log('y', data); + return data[2].value; + }, + startSymbol: { visible: true, symbolType: 'triangleDown', style: { size: 10, fill: '#f3a016' } }, + endSymbol: { visible: false }, + autoRange: true, + label: { + visible: true, + style: { dx: -4, dy: 0, fontSize: 12, fontWeight: 'normal', fill: '#fff', cursor: 'pointer' }, + position: 'insideStartTop', + labelBackground: { + visible: true, + padding: { left: 5, right: 5, top: 2, bottom: 2 }, + style: { fill: '#2F3B52', fillOpacity: 0.9, dx: -4, dy: 0 } + } + }, + line: { style: { stroke: '#f3a016', lineWidth: 2, lineDash: [3, 3], cursor: 'pointer' } }, + relativeSeriesId: 'mainSeries', + id: '7d14708c-de9d-49a1-919a-26c65ff95b42', + interactive: true + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await verifySourceSpec(page); + await page.evaluate(() => { + const chartSpace = window.__visualChart; + window.__beforeData = JSON.stringify( + chartSpace + .getChart() + .getAllSeries()[0] + .getViewData() + .latestData.map(d => [d.time, d.value]) + ); + chartSpace.updateFullDataSync([ + { + id: 'line', + values: [ + { time: '2:00', value: 8 }, + { time: '4:00', value: 9 }, + { time: '6:00', value: 20 }, + { time: '8:00', value: 14 }, + { time: '10:00', value: 16 }, + { time: '12:00', value: 17 }, + { time: '14:00', value: 17 }, + { time: '16:00', value: 16 }, + { time: '18:00', value: 15 } + ] + } + ]); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + data = c + .getChart() + .getAllSeries()[0] + .getViewData() + .latestData.map(d => [d.time, d.value]); + const expected = [ + ['2:00', 8], + ['4:00', 9], + ['6:00', 20], + ['8:00', 14], + ['10:00', 16], + ['12:00', 17], + ['14:00', 17], + ['16:00', 16], + ['18:00', 15] + ]; + if (JSON.stringify(data) !== JSON.stringify(expected) || JSON.stringify(data) === window.__beforeData) + throw Error('批量数据更新未生效'); + if ( + !c + .getChart() + .getAllComponents() + .some(c => c.type === 'markLine') + ) + throw Error('更新后标记线缺失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/api/update/update-legend-data.mjs b/packages/vchart/__tests__/visual/cases/api/update/update-legend-data.mjs new file mode 100644 index 0000000000..9c83273d52 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/api/update/update-legend-data.mjs @@ -0,0 +1,103 @@ +import { verifySpec, verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 6620e204d4511700f688557f + * 验证目的:图例选择后更新分类数据。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const data = [ + { value: 10, category: 'One' }, + { value: 9, category: 'Two' }, + { value: 6, category: 'Three' }, + { value: 5, category: 'Four' }, + { value: 4, category: 'Five' }, + { value: 3, category: 'Six' }, + { value: 1, category: 'Seven' } + ]; + const spec = { + type: 'pie', + data: [ + { + id: 'pie', + values: data + } + ], + categoryField: 'category', + valueField: 'value', + legends: { + visible: true, + orient: 'right', + item: { + width: '20%', + value: { + alignRight: true, + style: { + fill: '#333', + fillOpacity: 0.8, + fontSize: 10 + }, + state: { + unselected: { + fill: '#d8d8d8' + } + } + } + } + } + }; + + return spec; + }, + async exercise(page) { + // 保留源操作类型,并以真实实例或图元定位执行。 + await page.evaluate( + data => { + const c = window.__visualChart; + c.setLegendSelectedDataByIndex(0, ['One', 'Four']); + window.__selectedBeforeUpdate = c.getLegendSelectedDataByIndex(0); + c.updateDataSync('pie', data); + }, + [ + { value: 8, category: 'One000' }, + { value: 9, category: 'Two000' }, + { value: 60, category: 'Three' }, + { value: 50, category: 'Four' }, + { value: 32, category: 'Five' }, + { value: 30, category: 'Six' }, + { value: 12, category: 'Seven' } + ] + ); + }, + async verify(page) { + // 验证目标状态及实际绘制,动作缺失不能通过。 + await page.evaluate( + data => { + const c = window.__visualChart, + selected = c.getLegendSelectedDataByIndex(0); + if (JSON.stringify(window.__selectedBeforeUpdate) !== JSON.stringify(['One', 'Four'])) + throw Error('未先选择图例'); + const values = c.getChart().getAllSeries()[0].getViewData().latestData; + if ( + !values.length || + !values.some(d => d.category === 'Four' && d.value === 50) || + values.some(d => d.value === 5) + ) + throw Error('更新结果未生效'); + if (!selected.includes('Four')) throw Error('更新丢失仍然有效的图例选择'); + }, + [ + { value: 8, category: 'One000' }, + { value: 9, category: 'Two000' }, + { value: 60, category: 'Three' }, + { value: 50, category: 'Four' }, + { value: 32, category: 'Five' }, + { value: 30, category: 'Six' }, + { value: 12, category: 'Seven' } + ] + ); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-horizontal.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-horizontal.mjs new file mode 100644 index 0000000000..c7f0685662 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-horizontal.mjs @@ -0,0 +1,65 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ee252a1e9eec95f9e66 + * 验证目的:横向面积图的数据映射与坐标布局。 + * 保留条件:xField, yField, direction;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'value', + yField: 'time', + direction: 'horizontal' + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-invalid-zero.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-invalid-zero.mjs new file mode 100644 index 0000000000..22565d5e1a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-invalid-zero.mjs @@ -0,0 +1,86 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64d09d4eba8f75ea082eb5d7 + * 验证目的:缺失值按零处理的堆叠面积与标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const values = [ + { x: '0', type: 'A', y: '10' }, + { x: '1', type: 'A', y: '11' }, + { x: '2', type: 'A', y: undefined }, + { x: '3', type: 'A', y: '13' }, + { x: '4', type: 'A', y: '14' }, + { x: '5', type: 'A', y: '15' }, + { x: '6', type: 'A', y: '16' }, + { x: '7', type: 'A', y: '-17' }, + { x: '0', type: 'B', y: '21' }, + { x: '1', type: 'B', y: '22' }, + { x: '2', type: 'B', y: undefined }, + { x: '3', type: 'B', y: '24' }, + { x: '4', type: 'B', y: '25' }, + { x: '5', type: 'B', y: '26' }, + { x: '6', type: 'B', y: '27' }, + { x: '7', type: 'B', y: '-28' }, + { x: '0', type: 'C', y: '31' }, + { x: '1', type: 'C', y: '32' }, + { x: '2', type: 'C', y: undefined }, + { x: '3', type: 'C', y: '34' }, + { x: '4', type: 'C', y: '35' }, + { x: '5', type: 'C', y: '36' }, + { x: '6', type: 'C', y: '37' }, + { x: '7', type: 'C', y: '-38' } + ]; + const spec = { + type: 'area', + data: [ + { + id: 'id0', + values + } + ], + stack: true, + xField: 'x', + yField: 'y', + label: { visible: true }, + legends: [{ visible: true, position: 'middle', orient: 'left' }], + seriesField: 'type', + invalidType: 'zero', + line: { + style: {} + }, + point: { + state: { + hover: { + size: 20, + fill: 'red' + } + } + }, + axes: [ + { + orient: 'left', + tickCount: 6, + forceTickCount: 6, + visible: true + }, + { + // visible: false, + orient: 'bottom', + label: { visible: true } + } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-missing.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-missing.mjs new file mode 100644 index 0000000000..261db0bca4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-missing.mjs @@ -0,0 +1,721 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ee352a1e9eec95f9e68 + * 验证目的:面积图缺失值及源无效数据策略。 + * 保留条件:xField, yField, invalidType, point, title, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + month: '1980-05', + vacancies: 36.7 + }, + { + month: '1980-08', + vacancies: 37.5 + }, + { + month: '1980-11', + vacancies: 38.7 + }, + { + month: '1981-02', + vacancies: 37.4 + }, + { + month: '1981-05', + vacancies: 44 + }, + { + month: '1981-08', + vacancies: 44.5 + }, + { + month: '1981-11', + vacancies: 41.7 + }, + { + month: '1982-02', + vacancies: 37.3 + }, + { + month: '1982-05', + vacancies: 30.3 + }, + { + month: '1982-08', + vacancies: 26.8 + }, + { + month: '1982-11', + vacancies: 30.3 + }, + { + month: '1983-02', + vacancies: 29.7 + }, + { + month: '1983-05', + vacancies: 32.4 + }, + { + month: '1983-08', + vacancies: 33.6 + }, + { + month: '1983-11', + vacancies: 36.3 + }, + { + month: '1984-02', + vacancies: 44.6 + }, + { + month: '1984-05', + vacancies: 40.3 + }, + { + month: '1984-08', + vacancies: 48.5 + }, + { + month: '1984-11', + vacancies: 49.8 + }, + { + month: '1985-02', + vacancies: 60.4 + }, + { + month: '1985-05', + vacancies: 68.6 + }, + { + month: '1985-08', + vacancies: 67.2 + }, + { + month: '1985-11', + vacancies: 66.2 + }, + { + month: '1986-02', + vacancies: 64.8 + }, + { + month: '1986-05', + vacancies: 66.1 + }, + { + month: '1986-08', + vacancies: 61.1 + }, + { + month: '1986-11', + vacancies: 66.3 + }, + { + month: '1987-02', + vacancies: 69.5 + }, + { + month: '1987-05', + vacancies: 66.9 + }, + { + month: '1987-08', + vacancies: 70.1 + }, + { + month: '1987-11', + vacancies: 66.9 + }, + { + month: '1988-02', + vacancies: 70.2 + }, + { + month: '1988-05', + vacancies: 74.4 + }, + { + month: '1988-08', + vacancies: 80.8 + }, + { + month: '1988-11', + vacancies: 85.1 + }, + { + month: '1989-02', + vacancies: 84.7 + }, + { + month: '1989-05', + vacancies: 93.7 + }, + { + month: '1989-08', + vacancies: 76.6 + }, + { + month: '1989-11', + vacancies: 79.2 + }, + { + month: '1990-02', + vacancies: 71.9 + }, + { + month: '1990-05', + vacancies: 64 + }, + { + month: '1990-08', + vacancies: 56.7 + }, + { + month: '1990-11', + vacancies: 41.6 + }, + { + month: '1991-02', + vacancies: 33.8 + }, + { + month: '1991-05', + vacancies: 31.6 + }, + { + month: '1991-08', + vacancies: 29.8 + }, + { + month: '1991-11', + vacancies: 30.9 + }, + { + month: '1992-02', + vacancies: 33.3 + }, + { + month: '1992-05', + vacancies: 31.9 + }, + { + month: '1992-08', + vacancies: 33 + }, + { + month: '1992-11', + vacancies: 36.9 + }, + { + month: '1993-02', + vacancies: 37 + }, + { + month: '1993-05', + vacancies: 42 + }, + { + month: '1993-08', + vacancies: 46.2 + }, + { + month: '1993-11', + vacancies: 48.5 + }, + { + month: '1994-02', + vacancies: 57.6 + }, + { + month: '1994-05', + vacancies: 69.5 + }, + { + month: '1994-08', + vacancies: 82.2 + }, + { + month: '1994-11', + vacancies: 85.6 + }, + { + month: '1995-02', + vacancies: 72.7 + }, + { + month: '1995-05', + vacancies: 77.3 + }, + { + month: '1995-08', + vacancies: 74.6 + }, + { + month: '1995-11', + vacancies: 72.5 + }, + { + month: '1996-02', + vacancies: 81 + }, + { + month: '1996-05', + vacancies: 76.7 + }, + { + month: '1996-08', + vacancies: 77.5 + }, + { + month: '1996-11', + vacancies: 82.5 + }, + { + month: '1997-02', + vacancies: 81.3 + }, + { + month: '1997-05', + vacancies: 82.8 + }, + { + month: '1997-08', + vacancies: 84.9 + }, + { + month: '1997-11', + vacancies: 90.4 + }, + { + month: '1998-02', + vacancies: 98.3 + }, + { + month: '1998-05', + vacancies: 104 + }, + { + month: '1998-08', + vacancies: 89.8 + }, + { + month: '1998-11', + vacancies: 102 + }, + { + month: '1999-02', + vacancies: 87.2 + }, + { + month: '1999-05', + vacancies: 100.2 + }, + { + month: '1999-08', + vacancies: 106.9 + }, + { + month: '1999-11', + vacancies: 109.6 + }, + { + month: '2000-02', + vacancies: 118.1 + }, + { + month: '2000-05', + vacancies: 115.9 + }, + { + month: '2000-08', + vacancies: 114.4 + }, + { + month: '2000-11', + vacancies: 114.8 + }, + { + month: '2001-02', + vacancies: 99.7 + }, + { + month: '2001-05', + vacancies: 94 + }, + { + month: '2001-08', + vacancies: 90.2 + }, + { + month: '2001-11', + vacancies: 88.5 + }, + { + month: '2002-02', + vacancies: 90.5 + }, + { + month: '2002-05', + vacancies: 96.2 + }, + { + month: '2002-08', + vacancies: 103 + }, + { + month: '2002-11', + vacancies: 97.6 + }, + { + month: '2003-02', + vacancies: 109.7 + }, + { + month: '2003-05', + vacancies: 104.6 + }, + { + month: '2003-08', + vacancies: 104.1 + }, + { + month: '2003-11', + vacancies: 107.5 + }, + { + month: '2004-02', + vacancies: 103.8 + }, + { + month: '2004-05', + vacancies: 127.1 + }, + { + month: '2004-08', + vacancies: 124.6 + }, + { + month: '2004-11', + vacancies: 139.2 + }, + { + month: '2005-02', + vacancies: 146.2 + }, + { + month: '2005-05', + vacancies: 140.9 + }, + { + month: '2005-08', + vacancies: 139 + }, + { + month: '2005-11', + vacancies: 134.1 + }, + { + month: '2006-02', + vacancies: 144.3 + }, + { + month: '2006-05', + vacancies: 154.1 + }, + { + month: '2006-08', + vacancies: 154.9 + }, + { + month: '2006-11', + vacancies: 162.1 + }, + { + month: '2007-02', + vacancies: 160.8 + }, + { + month: '2007-05', + vacancies: 168.6 + }, + { + month: '2007-08', + vacancies: 173.3 + }, + { + month: '2007-11', + vacancies: 183.3 + }, + { + month: '2008-02', + vacancies: 178.2 + }, + { + month: '2008-05', + vacancies: 184.5 + }, + { + month: '2008-08', + vacancies: null + }, + { + month: '2008-11', + vacancies: null + }, + { + month: '2009-02', + vacancies: null + }, + { + month: '2009-05', + vacancies: null + }, + { + month: '2009-08', + vacancies: null + }, + { + month: '2009-11', + vacancies: 148.9 + }, + { + month: '2010-02', + vacancies: 168.4 + }, + { + month: '2010-05', + vacancies: 170.3 + }, + { + month: '2010-08', + vacancies: 178.6 + }, + { + month: '2010-11', + vacancies: 191.1 + }, + { + month: '2011-02', + vacancies: 189.4 + }, + { + month: '2011-05', + vacancies: 187.3 + }, + { + month: '2011-08', + vacancies: 183.4 + }, + { + month: '2011-11', + vacancies: 179.3 + }, + { + month: '2012-02', + vacancies: 181.8 + }, + { + month: '2012-05', + vacancies: 178.1 + }, + { + month: '2012-08', + vacancies: 175.3 + }, + { + month: '2012-11', + vacancies: 164.8 + }, + { + month: '2013-02', + vacancies: 149.8 + }, + { + month: '2013-05', + vacancies: 143.5 + }, + { + month: '2013-08', + vacancies: 140.6 + }, + { + month: '2013-11', + vacancies: 138.9 + }, + { + month: '2014-02', + vacancies: 143.2 + }, + { + month: '2014-05', + vacancies: 147.4 + }, + { + month: '2014-08', + vacancies: 146.6 + }, + { + month: '2014-11', + vacancies: 149.7 + }, + { + month: '2015-02', + vacancies: 151.9 + }, + { + month: '2015-05', + vacancies: 157.7 + }, + { + month: '2015-08', + vacancies: 161.7 + }, + { + month: '2015-11', + vacancies: 167.4 + }, + { + month: '2016-02', + vacancies: 172.2 + }, + { + month: '2016-05', + vacancies: 171.1 + }, + { + month: '2016-08', + vacancies: 177.5 + }, + { + month: '2016-11', + vacancies: 182.3 + }, + { + month: '2017-02', + vacancies: 185 + }, + { + month: '2017-05', + vacancies: 185.6 + }, + { + month: '2017-08', + vacancies: 200.9 + }, + { + month: '2017-11', + vacancies: 204.4 + }, + { + month: '2018-02', + vacancies: 212.8 + }, + { + month: '2018-05', + vacancies: 223.8 + }, + { + month: '2018-08', + vacancies: 228.7 + }, + { + month: '2018-11', + vacancies: 230.2 + }, + { + month: '2019-02', + vacancies: 232.4 + }, + { + month: '2019-05', + vacancies: 228.1 + }, + { + month: '2019-08', + vacancies: 224.3 + }, + { + month: '2019-11', + vacancies: 226.7 + }, + { + month: '2020-02', + vacancies: 227.4 + }, + { + month: '2020-05', + vacancies: 129.2 + }, + { + month: '2020-08', + vacancies: 206.3 + }, + { + month: '2020-11', + vacancies: 254.1 + }, + { + month: '2021-02', + vacancies: 288 + }, + { + month: '2021-05', + vacancies: 370.2 + }, + { + month: '2021-08', + vacancies: 333.9 + }, + { + month: '2021-11', + vacancies: 397.2 + }, + { + month: '2022-02', + vacancies: 421.9 + }, + { + month: '2022-05', + vacancies: 480.1 + } + ] + }, + xField: 'month', + yField: 'vacancies', + invalidType: 'break', + point: { + visible: false + }, + title: { + text: 'Job vacancies, seasonally adjusted', + subtext: 'Source: Australian Bureau of Statistics, 30 June 2022' + }, + axes: [ + { + orient: 'left', + title: { + visible: true, + text: 'thousands' + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-negative.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-negative.mjs new file mode 100644 index 0000000000..187cee1988 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-negative.mjs @@ -0,0 +1,64 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ee352a1e9eec95f9e67 + * 验证目的:面积图正负值与零基线。 + * 保留条件:xField, yField;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: -9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: -16 + }, + { + time: '12:00', + value: -17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value' + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-point-outer-border.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-point-outer-border.mjs new file mode 100644 index 0000000000..ae1fabb3b2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-point-outer-border.mjs @@ -0,0 +1,235 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64e845be707420a88d0e68ef + * 验证目的:面积图数据点外描边。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + point: { + style: { + size: 10, + outerBorder: { + distance: 0, + lineWidth: 4, + strokeOpacity: 0.2 + } + } + }, + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true + } + }, + seriesMark: 'point' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-series-mark.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-series-mark.mjs new file mode 100644 index 0000000000..e807654f50 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-series-mark.mjs @@ -0,0 +1,229 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64c869b481f8faaccbacb2d3 + * 验证目的:面积图指定主图元。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true + } + }, + seriesMark: 'point' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await page.evaluate(() => { + if (window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().name !== 'point') + throw Error('未使用 point 主图元'); + }); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-stacked.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-stacked.mjs new file mode 100644 index 0000000000..6cc2cdb801 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-stacked.mjs @@ -0,0 +1,221 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ee452a1e9eec95f9e6c + * 验证目的:多组面积图堆叠与图例。 + * 保留条件:title, stack, xField, yField, seriesField, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + stack: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-step-curve.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-step-curve.mjs new file mode 100644 index 0000000000..a1c99ca9d4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-step-curve.mjs @@ -0,0 +1,230 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66b0a9df27f96000ad549f20 + * 验证目的:面积图阶梯曲线。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true + } + }, + area: { + style: { + curveType: 'catmullRom' + } + }, + seriesMark: 'point' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-stream-center.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-stream-center.mjs new file mode 100644 index 0000000000..a7e544a03f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-stream-center.mjs @@ -0,0 +1,253 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0468 + * 验证目的:流图堆叠偏移和面积布局。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + data: { + id: 'data', + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + color: ['#F7FCFD', '#E0ECF4', '#BFD3E6', '#9EBCDA', '#8C96C5', '#8C6BB1', '#88419D', '#810F7C', '#4D004A'], + title: { + visible: true, + text: 'EVOLUTION OF BABY NAMES IN THE US' + }, + xField: 'country', + yField: 'value', + seriesField: 'type', + stackOffsetSilhouette: true, + point: { + visible: false + }, + area: { + style: { + fillOpacity: 0.4 + }, + state: { + hover: { + style: { + fillOpacity: 1 + } + } + } + }, + legends: [ + { + range: [], + visible: true, + position: 'middle', + orient: 'bottom', + padding: { + top: 30 + } + } + ], + axes: [ + { + orient: 'left', + visible: false + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/area/area-unstacked.mjs b/packages/vchart/__tests__/visual/cases/charts/area/area-unstacked.mjs new file mode 100644 index 0000000000..cb39d1e087 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/area/area-unstacked.mjs @@ -0,0 +1,221 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ee652a1e9eec95f9e6f + * 验证目的:多组面积图关闭堆叠后的相交布局。 + * 保留条件:title, stack, xField, yField, seriesField, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Unstacked area chart of cosmetic products sales' + }, + stack: false, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-gradient-corner.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-gradient-corner.mjs new file mode 100644 index 0000000000..f21ff6e8ee --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-gradient-corner.mjs @@ -0,0 +1,152 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7eed52a1e9eec95f9e7c + * 验证目的:渐变柱形与圆角。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'data', + values: [ + { + x: 'Mon', + y: 100, + type: 'Sales' + }, + { + x: 'Tues', + y: 66, + type: 'Sales' + }, + { + x: 'Wed', + y: 95, + type: 'Sales' + }, + { + x: 'Thus', + y: 52, + type: 'Sales' + }, + { + x: 'Fri', + y: 68, + type: 'Sales' + }, + { + x: 'Sat', + y: 52, + type: 'Sales' + }, + { + x: 'sun', + y: 48, + type: 'Sales' + }, + { + x: 'Mon', + y: 43, + type: 'Profit' + }, + { + x: 'Tues', + y: 80, + type: 'Profit' + }, + { + x: 'Wed', + y: 68, + type: 'Profit' + }, + { + x: 'Thus', + y: 40, + type: 'Profit' + }, + { + x: 'Fri', + y: 53, + type: 'Profit' + }, + { + x: 'Sat', + y: 72, + type: 'Profit' + }, + { + x: 'sun', + y: 71, + type: 'Profit' + } + ] + } + ], + xField: ['x', 'type'], + yField: 'y', + seriesField: 'type', + bar: { + style: { + cornerRadius: 10, + fill: { + gradient: 'linear', + x0: 0.5, + y0: 0, + x1: 0.5, + y1: 1, + stops: [ + { + offset: 0, + color: '#86DF6C' + }, + { + offset: 1, + color: '#468DFF' + } + ] + } + }, + state: { + selected: { + stroke: '#000', + lineWidth: 1 + } + } + }, + axes: [ + { + orient: 'bottom', + domainLine: { + visible: false + }, + bandPadding: 0, + paddingInner: 0.1 + }, + { + orient: 'left', + grid: { + visible: false + }, + tick: { + visible: true, + tickCount: 3 + }, + domainLine: { + visible: false + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-group-gap.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-group-gap.mjs new file mode 100644 index 0000000000..1a3b590291 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-group-gap.mjs @@ -0,0 +1,120 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64cc5eff378e90815866d456 + * 验证目的:分组柱的组内间距。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + color: ['#becef3', '#6a8edc', '#77caeb', '#52c93b', '#d3f5e8'], + data: [ + { + id: 'barData', + values: [ + { + type: 'A', + year: '2000', + value: 25 + }, + { + type: 'A', + year: '2010', + value: 28 + }, + { + type: 'A', + year: '2018', + value: 18 + }, + { + type: 'B', + year: '2000', + value: 23 + }, + { + type: 'B', + year: '2010', + value: 32 + }, + { + type: 'B', + year: '2018', + value: 22 + }, + { + type: 'C', + year: '2000', + value: 18 + }, + { + type: 'C', + year: '2010', + value: 18 + }, + { + type: 'C', + year: '2018', + value: 18 + }, + { + type: 'D', + year: '2000', + value: 15 + }, + { + type: 'D', + year: '2010', + value: 22 + }, + { + type: 'D', + year: '2018', + value: 19 + }, + { + type: 'E', + year: '2000', + value: 5 + }, + { + type: 'E', + year: '2010', + value: 12 + }, + { + type: 'E', + year: '2018', + value: 5 + } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + axes: [ + { + orient: 'bottom', + paddingInner: 0.3 + } + ], + bar: { + style: { + fillOpacity: 0.9 + } + }, + barWidth: 10, + barGapInGroup: '20%' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-group.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-group.mjs new file mode 100644 index 0000000000..dacb91a014 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-group.mjs @@ -0,0 +1,137 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7eed52a1e9eec95f9e7e + * 验证目的:分组柱图的双分类字段与系列。 + * 保留条件:xField, yField, seriesField, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + type: 'Autocracies', + year: '1930', + value: 129 + }, + { + type: 'Autocracies', + year: '1940', + value: 133 + }, + { + type: 'Autocracies', + year: '1950', + value: 130 + }, + { + type: 'Autocracies', + year: '1960', + value: 126 + }, + { + type: 'Autocracies', + year: '1970', + value: 117 + }, + { + type: 'Autocracies', + year: '1980', + value: 114 + }, + { + type: 'Autocracies', + year: '1990', + value: 111 + }, + { + type: 'Autocracies', + year: '2000', + value: 89 + }, + { + type: 'Autocracies', + year: '2010', + value: 80 + }, + { + type: 'Autocracies', + year: '2018', + value: 80 + }, + { + type: 'Democracies', + year: '1930', + value: 22 + }, + { + type: 'Democracies', + year: '1940', + value: 13 + }, + { + type: 'Democracies', + year: '1950', + value: 25 + }, + { + type: 'Democracies', + year: '1960', + value: 29 + }, + { + type: 'Democracies', + year: '1970', + value: 38 + }, + { + type: 'Democracies', + year: '1980', + value: 41 + }, + { + type: 'Democracies', + year: '1990', + value: 57 + }, + { + type: 'Democracies', + year: '2000', + value: 87 + }, + { + type: 'Democracies', + year: '2010', + value: 98 + }, + { + type: 'Democracies', + year: '2018', + value: 99 + } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + legends: { + visible: true, + orient: 'top', + position: 'start' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-min-width.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-min-width.mjs new file mode 100644 index 0000000000..72073fbce7 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-min-width.mjs @@ -0,0 +1,566 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65dc768cdb5fe000ad96fd99 + * 验证目的:最小柱宽与自动带宽。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + date: '2019-08-29', + group: 'Cake', + value: 154, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Bread', + value: 378, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Tea', + value: 103, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Coffee', + value: 310, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Rib', + value: 419, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Crayfish', + value: 810, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Cake', + value: 153, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Bread', + value: 398, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Tea', + value: 105, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Coffee', + value: 298, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Rib', + value: 416, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Crayfish', + value: 796, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Cake', + value: 151, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Bread', + value: 408, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Tea', + value: 110, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Coffee', + value: 302, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Rib', + value: 400, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Crayfish', + value: 811, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Cake', + value: 135, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Bread', + value: 407, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Tea', + value: 944, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Coffee', + value: 298, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Rib', + value: 343, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Crayfish', + value: 771, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Cake', + value: 997, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Bread', + value: 363, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Tea', + value: 636, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Coffee', + value: 239, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Rib', + value: 204, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Crayfish', + value: 641, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Cake', + value: 984, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Bread', + value: 356, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Tea', + value: 627, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Coffee', + value: 241, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Rib', + value: 231, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Crayfish', + value: 646, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Cake', + value: 943, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Bread', + value: 355, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Tea', + value: 611, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Coffee', + value: 259, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Rib', + value: 230, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Crayfish', + value: 666, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Cake(last week)', + value: -365, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Bread(last week)', + value: -235, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Tea(last week)', + value: -832, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Coffee(last week)', + value: -610, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Rib(last week)', + value: -305, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Crayfish(last week)', + value: -462, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Cake(last week)', + value: -522, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Bread(last week)', + value: -258, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Tea(last week)', + value: -689, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Coffee(last week)', + value: -688, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Rib(last week)', + value: -106, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Crayfish(last week)', + value: -159, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Cake(last week)', + value: -352, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Bread(last week)', + value: -760, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Tea(last week)', + value: -332, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Coffee(last week)', + value: -368, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Rib(last week)', + value: -222, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Crayfish(last week)', + value: -205, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Cake(last week)', + value: -471, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Bread(last week)', + value: -535, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Tea(last week)', + value: -319, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Coffee(last week)', + value: -363, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Rib(last week)', + value: -243, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Crayfish(last week)', + value: -129, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Cake(last week)', + value: -319, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Bread(last week)', + value: -570, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Tea(last week)', + value: -532, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Coffee(last week)', + value: -312, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Rib(last week)', + value: -583, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Crayfish(last week)', + value: -342, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Cake(last week)', + value: -346, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Bread(last week)', + value: -373, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Tea(last week)', + value: -582, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Coffee(last week)', + value: -247, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Rib(last week)', + value: -294, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Crayfish(last week)', + value: -165, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Cake(last week)', + value: -326, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Bread(last week)', + value: -879, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Tea(last week)', + value: -219, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Coffee(last week)', + value: -236, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Rib(last week)', + value: -153, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Crayfish(last week)', + value: -253, + stack: 'Meat dishes' + } + ] + } + ], + barMinWidth: 30, + autoBandSize: { + extend: 0 + }, + scrollBar: { + orient: 'bottom', + auto: true + }, + xField: ['date', 'stack'], + yField: 'value', + seriesField: 'group', + stack: true, + stackCornerRadius: 1000, + axes: [ + { + orient: 'left', + title: { + visible: true, + text: 'Week-on-week (sales)' + }, + tick: { + tickCount: 10 + } + }, + { + orient: 'bottom', + domainLine: { + onZero: true + } + } + ], + legends: { + visible: true + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-percent.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-percent.mjs new file mode 100644 index 0000000000..24e83f5d7e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-percent.mjs @@ -0,0 +1,126 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7eef52a1e9eec95f9e84 + * 验证目的:百分比堆叠柱图与百分数刻度格式。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + 年龄段: 'Under 5', + 人口数量: 25635 + }, + { + State: 'WY', + 年龄段: '5 to 13', + 人口数量: 1890 + }, + { + State: 'WY', + 年龄段: '14 to 17', + 人口数量: 9314 + }, + { + State: 'DC', + 年龄段: 'Under 5', + 人口数量: 30352 + }, + { + State: 'DC', + 年龄段: '5 to 13', + 人口数量: 20439 + }, + { + State: 'DC', + 年龄段: '14 to 17', + 人口数量: 10225 + }, + { + State: 'VT', + 年龄段: 'Under 5', + 人口数量: 38253 + }, + { + State: 'VT', + 年龄段: '5 to 13', + 人口数量: 42538 + }, + { + State: 'VT', + 年龄段: '14 to 17', + 人口数量: 15757 + }, + { + State: 'ND', + 年龄段: 'Under 5', + 人口数量: 51896 + }, + { + State: 'ND', + 年龄段: '5 to 13', + 人口数量: 67358 + }, + { + State: 'ND', + 年龄段: '14 to 17', + 人口数量: 18794 + }, + { + State: 'AK', + 年龄段: 'Under 5', + 人口数量: 72083 + }, + { + State: 'AK', + 年龄段: '5 to 13', + 人口数量: 85640 + }, + { + State: 'AK', + 年龄段: '14 to 17', + 人口数量: 22153 + } + ] + } + ], + xField: 'State', + yField: '人口数量', + seriesField: '年龄段', + percent: true, + stack: true, + legends: { + visible: true + }, + axes: [ + { + orient: 'left', + label: { + formatMethod: val => { + return `${(val * 100).toFixed(2)}%`; + } + } + } + ], + tooltip: { + mark: { visible: false } + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-corner.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-corner.mjs new file mode 100644 index 0000000000..d61b98b079 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-corner.mjs @@ -0,0 +1,567 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65dc75d4603de700d1d8b987 + * 验证目的:堆叠柱整体圆角和自动带宽。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + date: '2019-08-29', + group: 'Cake', + value: 154, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Bread', + value: 378, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Tea', + value: 103, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Coffee', + value: 310, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Rib', + value: 419, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Crayfish', + value: 810, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Cake', + value: 153, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Bread', + value: 398, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Tea', + value: 105, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Coffee', + value: 298, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Rib', + value: 416, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Crayfish', + value: 796, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Cake', + value: 151, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Bread', + value: 408, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Tea', + value: 110, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Coffee', + value: 302, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Rib', + value: 400, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Crayfish', + value: 811, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Cake', + value: 135, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Bread', + value: 407, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Tea', + value: 944, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Coffee', + value: 298, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Rib', + value: 343, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Crayfish', + value: 771, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Cake', + value: 997, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Bread', + value: 363, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Tea', + value: 636, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Coffee', + value: 239, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Rib', + value: 204, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Crayfish', + value: 641, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Cake', + value: 984, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Bread', + value: 356, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Tea', + value: 627, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Coffee', + value: 241, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Rib', + value: 231, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Crayfish', + value: 646, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Cake', + value: 943, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Bread', + value: 355, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Tea', + value: 611, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Coffee', + value: 259, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Rib', + value: 230, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Crayfish', + value: 666, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Cake(last week)', + value: -365, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Bread(last week)', + value: -235, + stack: 'Dessert' + }, + { + date: '2019-08-29', + group: 'Tea(last week)', + value: -832, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Coffee(last week)', + value: -610, + stack: 'Drink' + }, + { + date: '2019-08-29', + group: 'Rib(last week)', + value: -305, + stack: 'Meat dishes' + }, + { + date: '2019-08-29', + group: 'Crayfish(last week)', + value: -462, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Cake(last week)', + value: -522, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Bread(last week)', + value: -258, + stack: 'Dessert' + }, + { + date: '2019-08-30', + group: 'Tea(last week)', + value: -689, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Coffee(last week)', + value: -688, + stack: 'Drink' + }, + { + date: '2019-08-30', + group: 'Rib(last week)', + value: -106, + stack: 'Meat dishes' + }, + { + date: '2019-08-30', + group: 'Crayfish(last week)', + value: -159, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Cake(last week)', + value: -352, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Bread(last week)', + value: -760, + stack: 'Dessert' + }, + { + date: '2019-08-31', + group: 'Tea(last week)', + value: -332, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Coffee(last week)', + value: -368, + stack: 'Drink' + }, + { + date: '2019-08-31', + group: 'Rib(last week)', + value: -222, + stack: 'Meat dishes' + }, + { + date: '2019-08-31', + group: 'Crayfish(last week)', + value: -205, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Cake(last week)', + value: -471, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Bread(last week)', + value: -535, + stack: 'Dessert' + }, + { + date: '2019-09-01', + group: 'Tea(last week)', + value: -319, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Coffee(last week)', + value: -363, + stack: 'Drink' + }, + { + date: '2019-09-01', + group: 'Rib(last week)', + value: -243, + stack: 'Meat dishes' + }, + { + date: '2019-09-01', + group: 'Crayfish(last week)', + value: -129, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Cake(last week)', + value: -319, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Bread(last week)', + value: -570, + stack: 'Dessert' + }, + { + date: '2019-09-02', + group: 'Tea(last week)', + value: -532, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Coffee(last week)', + value: -312, + stack: 'Drink' + }, + { + date: '2019-09-02', + group: 'Rib(last week)', + value: -583, + stack: 'Meat dishes' + }, + { + date: '2019-09-02', + group: 'Crayfish(last week)', + value: -342, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Cake(last week)', + value: -346, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Bread(last week)', + value: -373, + stack: 'Dessert' + }, + { + date: '2019-09-03', + group: 'Tea(last week)', + value: -582, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Coffee(last week)', + value: -247, + stack: 'Drink' + }, + { + date: '2019-09-03', + group: 'Rib(last week)', + value: -294, + stack: 'Meat dishes' + }, + { + date: '2019-09-03', + group: 'Crayfish(last week)', + value: -165, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Cake(last week)', + value: -326, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Bread(last week)', + value: -879, + stack: 'Dessert' + }, + { + date: '2019-09-04', + group: 'Tea(last week)', + value: -219, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Coffee(last week)', + value: -236, + stack: 'Drink' + }, + { + date: '2019-09-04', + group: 'Rib(last week)', + value: -153, + stack: 'Meat dishes' + }, + { + date: '2019-09-04', + group: 'Crayfish(last week)', + value: -253, + stack: 'Meat dishes' + } + ] + } + ], + barWidth: 5, + barGapInGroup: 0, + autoBandSize: { + extend: 10 + }, + scrollBar: { + orient: 'bottom', + auto: true + }, + xField: ['date', 'stack'], + yField: 'value', + seriesField: 'group', + stack: true, + stackCornerRadius: 1000, + axes: [ + { + orient: 'left', + title: { + visible: true, + text: 'Week-on-week (sales)' + }, + tick: { + tickCount: 10 + } + }, + { + orient: 'bottom', + domainLine: { + onZero: true + } + } + ], + legends: { + visible: true + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-inverse-value.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-inverse-value.mjs new file mode 100644 index 0000000000..5262d2a940 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-inverse-value.mjs @@ -0,0 +1,129 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65e0335b1baf7300adb86304 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:堆叠柱图反转数值轴并显示总计标签。 + * 保留条件:stack、左轴 inverse、totalLabel、15 条原始数据。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + Age: 'Under 5 Years', + Population: 25635 + }, + { + State: 'WY', + Age: '5 to 13 Years', + Population: 1890 + }, + { + State: 'WY', + Age: '14 to 17 Years', + Population: 9314 + }, + { + State: 'DC', + Age: 'Under 5 Years', + Population: 30352 + }, + { + State: 'DC', + Age: '5 to 13 Years', + Population: 20439 + }, + { + State: 'DC', + Age: '14 to 17 Years', + Population: 10225 + }, + { + State: 'VT', + Age: 'Under 5 Years', + Population: 38253 + }, + { + State: 'VT', + Age: '5 to 13 Years', + Population: 42538 + }, + { + State: 'VT', + Age: '14 to 17 Years', + Population: 15757 + }, + { + State: 'ND', + Age: 'Under 5 Years', + Population: 51896 + }, + { + State: 'ND', + Age: '5 to 13 Years', + Population: 67358 + }, + { + State: 'ND', + Age: '14 to 17 Years', + Population: 18794 + }, + { + State: 'AK', + Age: 'Under 5 Years', + Population: 72083 + }, + { + State: 'AK', + Age: '5 to 13 Years', + Population: 85640 + }, + { + State: 'AK', + Age: '14 to 17 Years', + Population: 22153 + } + ] + } + ], + xField: 'State', + yField: 'Population', + seriesField: 'Age', + stack: true, + legends: { + visible: true + }, + bar: { + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + }, + totalLabel: { + visible: true + }, + axes: [ + { + orient: 'left', + inverse: true + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-sort-min-height.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-sort-min-height.mjs new file mode 100644 index 0000000000..af5c408128 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack-sort-min-height.mjs @@ -0,0 +1,57 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 660b8281e46a4800b369f7e1 + * 验证目的:多数据源堆叠排序、逆序与最小柱高。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'common', + stackSort: true, + stackInverse: true, + height: 500, + data: [ + { + id: '1', + values: [ + { x: 1, y: 2, t: 'a' }, + { x: 1, y: 20, t: 'b' }, + { x: 1, y: 20, t: 'c' }, + { x: 2, y: 20, t: 'b' }, + { x: 2, y: 20, t: 'c' }, + { x: 2, y: 20, t: 'd' } + ] + }, + { + id: '2', + values: [ + { x: 1, y: 2, t: 'd' }, + { x: 1, y: 20, t: 'b' }, + { x: 1, y: 2, t: 'c' }, + { x: 2, y: 20, t: 'd' }, + { x: 2, y: 40, t: 'b' }, + { x: 2, y: 2, t: 'a' } + ] + } + ], + series: [ + { xField: 'x', yField: 'y', seriesField: 't', stack: true, type: 'bar', dataIndex: 0, barMinHeight: 50 }, + { xField: 'x', yField: 'y', seriesField: 't', stack: true, type: 'bar', dataIndex: 1, barMinHeight: 50 } + ], + axes: [ + { orient: 'left', type: 'linear', max: 300 }, + { orient: 'bottom', type: 'band' } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack.mjs new file mode 100644 index 0000000000..75debaa073 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-stack.mjs @@ -0,0 +1,17 @@ +import { barSpec, verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:正负值、堆叠与零基准线(bar)。 + * 图表类型:bar。 + * 关键配置:stack、seriesField,使用默认坐标轴。 + * 场景条件:固定两组数据,包含正负值。 + * 最终检查:核对堆叠 spec、有效绘制与最终截图。 + * 覆盖边界:不验证百分比堆叠或数据更新。 + */ +export default { + createSpec: barSpec, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-three-level-gap.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-three-level-gap.mjs new file mode 100644 index 0000000000..3e0cb57e61 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-three-level-gap.mjs @@ -0,0 +1,102 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64cd376ca77f904af107fe71 + * 验证目的:三层分组柱图的组内间距。 + * 保留条件:xField, yField, seriesField, barGapInGroup, barWidth, paddingInner, bandPadding, label;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + data: [ + { + values: [ + { + type: 'Category One', + min: 76, + max: 100, + range: 'A', + type2: 'p', + color: 'A_p' + }, + { + type: 'Category Two', + min: 56, + max: 108, + range: 'A', + type2: 'p', + color: 'A_p' + }, + { + type: 'Category One', + min: 56, + max: 100, + range: 'B', + type2: 'p', + color: 'B_p' + }, + { + type: 'Category Two', + min: 36, + max: 108, + range: 'B', + type2: 'p', + color: 'B_p' + }, + { + type: 'Category One', + min: 76, + max: 100, + range: 'A', + type2: 'k', + color: 'A_k' + }, + { + type: 'Category Two', + min: 56, + max: 108, + range: 'A', + type2: 'k', + color: 'A_k' + }, + { + type: 'Category One', + min: 56, + max: 100, + range: 'B', + type2: 'k', + color: 'B_k' + }, + { + type: 'Category Two', + min: 36, + max: 108, + range: 'B', + type2: 'k', + color: 'B_k' + } + ] + } + ], + xField: ['type', 'range', 'type2'], + yField: 'min', + seriesField: 'color', + barGapInGroup: [0, 5], + barWidth: 20, + paddingInner: [0.6, 0.6, 0.6], + bandPadding: [0.6, 0.6, 0.6], + label: { + position: 'bothEnd' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/bar/bar-width-limit.mjs b/packages/vchart/__tests__/visual/cases/charts/bar/bar-width-limit.mjs new file mode 100644 index 0000000000..3e3fb1ef2f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/bar/bar-width-limit.mjs @@ -0,0 +1,53 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66864e5b3160d400b5ccedad + * 验证目的:柱宽与最大柱宽共同配置。 + * 保留条件:barWidth, barMaxWidth, xField, yField;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + barWidth: 50, + barMaxWidth: 10, + data: [ + { + id: 'barData', + values: [ + { + month: 'Monday', + sales: 22 + }, + { + month: 'Tuesday', + sales: 13 + }, + { + month: 'Wednesday', + sales: 25 + }, + { + month: 'Thursday', + sales: 29 + }, + { + month: 'Friday', + sales: 38 + } + ] + } + ], + xField: 'month', + yField: 'sales' + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-bar-shaft.mjs b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-bar-shaft.mjs new file mode 100644 index 0000000000..b6c99a330d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-bar-shaft.mjs @@ -0,0 +1,163 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7ef052a1e9eec95f9e86 + * 验证目的:箱线图柱形须线和宽度。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'boxPlot', + data: [ + { + name: 'boxPlot', + values: [ + { + x: 'Sub-Saharan Africa', + y1: 9.16270124, + y2: 10.07530816, + y3: 10.09027907, + y4: 10.27579542, + y5: 11.62222959, + group: 'High income' + }, + { + x: 'Sub-Saharan Africa', + y1: 8.721525214, + y2: 9.641352839, + y3: 10.1736233, + y4: 10.57169914, + y5: 11.64427467, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.404757278, + y2: 10.36482449, + y3: 10.94903493, + y4: 11.5806383, + y5: 12.50192084, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.732841997, + y2: 9.732841997, + y3: 9.732841997, + y4: 9.732841997, + y5: 9.732841997, + group: 'High income' + }, + { + x: 'Middle East & North Africa', + y1: 9.541951901, + y2: 10.33719892, + y3: 10.91206173, + y4: 11.29821858, + y5: 11.60653481, + group: 'Low income' + }, + { + x: 'Middle East & North Africa', + y1: 10.2396509, + y2: 10.63879995, + y3: 11.09996104, + y4: 11.54301107, + y5: 11.92092709, + group: 'High income' + }, + { + x: 'Latin America & Caribbean', + y1: 10.14653181, + y2: 10.32106777, + y3: 10.45467215, + y4: 10.45844052, + y5: 10.6064696, + group: 'Low income' + }, + { + x: 'Latin America & Caribbean', + y1: 8.743652009, + y2: 9.413881158, + y3: 10.16606248, + y4: 11.00011805, + y5: 12.20655104, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 7.800035977, + y2: 8.850646235, + y3: 10.14633178, + y4: 11.59877618, + y5: 13.24880824, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 8.316035904, + y2: 9.038602613, + y3: 10.22954548, + y4: 10.71782871, + y5: 12.07411874, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.831631935, + y2: 9.939275167, + y3: 10.39108655, + y4: 10.95556656, + y5: 11.3012157, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.522480948, + y2: 10.43085982, + y3: 11.06642694, + y4: 11.73822523, + y5: 12.62940296, + group: 'High income' + } + ] + } + ], + xField: ['x', 'group'], + minField: 'y1', + q1Field: 'y2', + medianField: 'y3', + q3Field: 'y4', + maxField: 'y5', + seriesField: 'group', + direction: 'vertical', + color: ['#BBD6B8', '#EA5455'], + legends: [ + { + visible: true + } + ], + title: { + visible: true, + text: 'Global GDP 2021' + }, + boxPlot: { + style: { + boxWidth: 50, + shaftWidth: 30, + shaftShape: 'bar', + lineWidth: 2, + shaftOpacity: 0.3 + } + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-grouped.mjs b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-grouped.mjs new file mode 100644 index 0000000000..f627138090 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-grouped.mjs @@ -0,0 +1,168 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ef052a1e9eec95f9e87 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:分组箱线图与图例标题及分层轴间距。 + * 保留条件:12 个原始集合、双 xField、group、paddingInner、颜色。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'boxPlot', + data: [ + { + name: 'boxPlot', + values: [ + { + x: 'Sub-Saharan Africa', + y1: 9.16270124, + y2: 10.07530816, + y3: 10.09027907, + y4: 10.27579542, + y5: 11.62222959, + group: 'High income' + }, + { + x: 'Sub-Saharan Africa', + y1: 8.721525214, + y2: 9.641352839, + y3: 10.1736233, + y4: 10.57169914, + y5: 11.64427467, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.404757278, + y2: 10.36482449, + y3: 10.94903493, + y4: 11.5806383, + y5: 12.50192084, + group: 'Low income' + }, + { + x: 'South Asia', + y1: 9.732841997, + y2: 9.732841997, + y3: 9.732841997, + y4: 9.732841997, + y5: 9.732841997, + group: 'High income' + }, + { + x: 'Middle East & North Africa', + y1: 9.541951901, + y2: 10.33719892, + y3: 10.91206173, + y4: 11.29821858, + y5: 11.60653481, + group: 'Low income' + }, + { + x: 'Middle East & North Africa', + y1: 10.2396509, + y2: 10.63879995, + y3: 11.09996104, + y4: 11.54301107, + y5: 11.92092709, + group: 'High income' + }, + { + x: 'Latin America & Caribbean', + y1: 10.14653181, + y2: 10.32106777, + y3: 10.45467215, + y4: 10.45844052, + y5: 10.6064696, + group: 'Low income' + }, + { + x: 'Latin America & Caribbean', + y1: 8.743652009, + y2: 9.413881158, + y3: 10.16606248, + y4: 11.00011805, + y5: 12.20655104, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 7.800035977, + y2: 8.850646235, + y3: 10.14633178, + y4: 11.59877618, + y5: 13.24880824, + group: 'High income' + }, + { + x: 'East Asia & Pacific', + y1: 8.316035904, + y2: 9.038602613, + y3: 10.22954548, + y4: 10.71782871, + y5: 12.07411874, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.831631935, + y2: 9.939275167, + y3: 10.39108655, + y4: 10.95556656, + y5: 11.3012157, + group: 'Low income' + }, + { + x: 'Europe & Central Asia', + y1: 9.522480948, + y2: 10.43085982, + y3: 11.06642694, + y4: 11.73822523, + y5: 12.62940296, + group: 'High income' + } + ] + } + ], + xField: ['x', 'group'], + minField: 'y1', + q1Field: 'y2', + medianField: 'y3', + q3Field: 'y4', + maxField: 'y5', + seriesField: 'group', + direction: 'vertical', + color: ['#62CDFF', '#9E4784'], + legends: [ + { + visible: true + } + ], + title: { + visible: true, + text: 'Global GDP 2021' + }, + boxPlot: { + style: { + shaftShape: 'line', + lineWidth: 2 + } + }, + axes: [ + { + orient: 'bottom', + paddingInner: [0.1, 0.4] + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-outliers.mjs b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-outliers.mjs new file mode 100644 index 0000000000..5035ae4a9d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-outliers.mjs @@ -0,0 +1,135 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0436 + * 验证目的:横向箱线图正负异常点。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'boxPlot', + data: [ + { + name: 'boxPlot', + fields: { + y1: { + alias: 'Maximum' + }, + y5: { + alias: 'Minimum' + } + }, + values: [ + { + x: 'group1', + y1: 1600, + y2: 1200, + y3: 800, + y4: 700, + y5: 500, + outliers: [2000, 2200] + }, + { + x: 'group2', + y1: 1900, + y2: 1000, + y3: 400, + y4: 300, + y5: 100, + outliers: [2500] + }, + { + x: 'group3', + y1: 1300, + y2: 1200, + y3: 200, + y4: -100, + y5: -500, + outliers: [-1000, 1800, 2300] + }, + { + x: 'group4', + y1: 1400, + y2: 1000, + y3: 900, + y4: 800, + y5: 500, + outliers: [-2000] + }, + { + x: 'group5', + y1: 1200, + y2: 500, + y3: 400, + y4: -100, + y5: -400, + outliers: [-500, -1340] + }, + { + x: 'group6', + y1: 1400, + y2: 1000, + y3: 900, + y4: 700, + y5: 300, + outliers: [3200] + } + ] + } + ], + yField: 'x', + height: 500, + minField: 'y5', + q1Field: 'y4', + medianField: 'y3', + q3Field: 'y2', + maxField: 'y1', + outliersField: 'outliers', + direction: 'horizontal', + outliersStyle: { + fill: '#FE6244', + size: 10 + }, + crosshair: { + trigger: ['click', 'hover'], + yField: { + line: { + visible: true, + type: 'rect', + width: '150%' + }, + label: { + visible: false + } + } + }, + boxPlot: { + style: { + shaftFillOpacity: 0.5, + lineWidth: 2, + shaftWidth: 50, + stroke: '#62CDFF', + boxFill: '#9E4784', + shaftShape: 'bar' + }, + state: { + selected: { + stroke: 'yellow', + lineWidth: 10 + }, + hover: { + stroke: 'blue', + lineWidth: 10 + } + } + } + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-vertical.mjs b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-vertical.mjs new file mode 100644 index 0000000000..3f1be9bc44 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/box-plot/box-plot-vertical.mjs @@ -0,0 +1,91 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ef052a1e9eec95f9e85 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:纵向箱线图五数概括与线形须线。 + * 保留条件:六个原始五数集合、shaftShape=line、lineWidth=2。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'boxPlot', + data: [ + { + name: 'boxPlot', + values: [ + { + x: 'Sub-Saharan Africa', + y1: 8.72, + y2: 9.73, + y3: 10.17, + y4: 10.51, + y5: 11.64 + }, + { + x: 'South Asia', + y1: 9.4, + y2: 10.06, + y3: 10.75, + y4: 11.56, + y5: 12.5 + }, + { + x: 'Middle East & North Africa', + y1: 9.54, + y2: 10.6, + y3: 11.05, + y4: 11.5, + y5: 11.92 + }, + { + x: 'Latin America & Caribbean', + y1: 8.74, + y2: 9.46, + y3: 10.35, + y4: 10.94, + y5: 12.21 + }, + { + x: 'East Asia & Pacific', + y1: 7.8, + y2: 8.95, + y3: 10.18, + y4: 11.57, + y5: 13.25 + }, + { + x: 'Europe & Central Asia', + y1: 9.52, + y2: 10.39, + y3: 10.93, + y4: 11.69, + y5: 12.63 + } + ] + } + ], + xField: 'x', + minField: 'y1', + q1Field: 'y2', + medianField: 'y3', + q3Field: 'y4', + maxField: 'y5', + direction: 'vertical', + boxPlot: { + style: { + shaftShape: 'line', + lineWidth: 2 + } + } + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-event.mjs b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-event.mjs new file mode 100644 index 0000000000..535ee50c6c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-event.mjs @@ -0,0 +1,259 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65fd3b127c3a0c00c99f2ebe + * 迁移边界:源未录制鼠标;使用实际图元验证源 drill:true 的下钻与逐层回退,不宣称重放历史录制。 + * 验证目的:来源层级图下钻与空白回退的路径和实际布局。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { + name: 'root', + children: [ + { + name: 'Country A', + children: [ + { + name: 'Region1', + children: [ + { name: 'Office Supplies', value: 824 }, + { name: 'Furniture', value: 920 }, + { name: 'Electronic equipment', value: 936 } + ] + }, + { + name: 'Region2', + children: [ + { name: 'Office Supplies', value: 1270 }, + { name: 'Furniture', value: 1399 }, + { name: 'Electronic equipment', value: 1466 } + ] + }, + { + name: 'Region3', + children: [ + { name: 'Office Supplies', value: 1408 }, + { name: 'Furniture', value: 1676 }, + { name: 'Electronic equipment', value: 1559 } + ] + }, + { + name: 'Region4', + children: [ + { name: 'Office Supplies', value: 745 }, + { name: 'Furniture', value: 919 }, + { name: 'Electronic equipment', value: 781 } + ] + }, + { + name: 'Region5', + children: [ + { name: 'Office Supplies', value: 267 }, + { name: 'Furniture', value: 316 }, + { name: 'Electronic equipment', value: 230 } + ] + }, + { + name: 'Region6', + children: [ + { name: 'Office Supplies', value: 347 }, + { name: 'Furniture', value: 501 }, + { name: 'Electronic equipment', value: 453 } + ] + } + ] + }, + { + name: 'Country B', + children: [ + { + name: 'Region1', + children: [ + { name: 'Office Supplies', value: 824 }, + { name: 'Furniture', value: 920 }, + { name: 'Electronic equipment', value: 936 } + ] + }, + { + name: 'Region2', + children: [ + { name: 'Office Supplies', value: 1270 }, + { name: 'Furniture', value: 1399 }, + { name: 'Electronic equipment', value: 1466 } + ] + }, + { + name: 'Region3', + children: [ + { name: 'Office Supplies', value: 1408 }, + { name: 'Furniture', value: 1676 }, + { name: 'Electronic equipment', value: 1559 } + ] + }, + { + name: 'Region4', + children: [ + { name: 'Office Supplies', value: 745 }, + { name: 'Furniture', value: 919 }, + { name: 'Electronic equipment', value: 781 } + ] + }, + { + name: 'Region5', + children: [ + { name: 'Office Supplies', value: 267 }, + { name: 'Furniture', value: 316 }, + { name: 'Electronic equipment', value: 230 } + ] + }, + { + name: 'Region6', + children: [ + { name: 'Office Supplies', value: 347 }, + { name: 'Furniture', value: 501 }, + { name: 'Electronic equipment', value: 453 } + ] + } + ] + }, + { + name: 'Country C', + children: [ + { + name: 'Region1', + children: [ + { name: 'Office Supplies', value: 824 }, + { name: 'Furniture', value: 920 }, + { name: 'Electronic equipment', value: 936 } + ] + }, + { + name: 'Region2', + children: [ + { name: 'Office Supplies', value: 1270 }, + { name: 'Furniture', value: 1399 }, + { name: 'Electronic equipment', value: 1466 } + ] + }, + { + name: 'Region3', + children: [ + { name: 'Office Supplies', value: 1408 }, + { name: 'Furniture', value: 1676 }, + { name: 'Electronic equipment', value: 1559 } + ] + }, + { + name: 'Region4', + children: [ + { name: 'Office Supplies', value: 745 }, + { name: 'Furniture', value: 919 }, + { name: 'Electronic equipment', value: 781 } + ] + }, + { + name: 'Region5', + children: [ + { name: 'Office Supplies', value: 267 }, + { name: 'Furniture', value: 316 }, + { name: 'Electronic equipment', value: 230 } + ] + }, + { + name: 'Region6', + children: [ + { name: 'Office Supplies', value: 347 }, + { name: 'Furniture', value: 501 }, + { name: 'Electronic equipment', value: 453 } + ] + } + ] + } + ] + } + ]; + const spec = { + data: [{ id: 'data', values: data }], + type: 'circlePacking', + categoryField: 'name', + valueField: 'value', + drill: true, + circlePacking: { style: { fillOpacity: d => (d.isLeaf ? 0.75 : 0.25) } }, + layoutPadding: [0, 10, 10], + label: { + style: { + fontSize: 10, + visible: d => { + return d.depth === 1; + } + } + }, + animationEnter: { easing: 'cubicInOut' }, + animationExit: { easing: 'cubicInOut' }, + animationUpdate: { easing: 'cubicInOut' }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__drillEvents = []; + window.__hierarchyBefore = JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + ); + c.on('drill', e => window.__drillEvents.push({ type: e.value.type, path: [...e.value.path] })); + }); + const p = await interactionTarget(page, 'circlePacking', 1); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + await page.evaluate( + () => + (window.__hierarchyAfter = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + )) + ); + await page.mouse.click(790, 590); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const e = window.__drillEvents; + if ( + e?.length !== 2 || + !e[0].path.length || + e[1].path.length >= e[0].path.length || + window.__hierarchyBefore === window.__hierarchyAfter + ) + throw Error('层级下钻/回退没有改变路径与布局'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-forest.mjs b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-forest.mjs new file mode 100644 index 0000000000..142aea7ff5 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-forest.mjs @@ -0,0 +1,253 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7ef552a1e9eec95f9e93 + * 验证目的:来源层级图下钻与空白回退的路径和实际布局。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { + name: 'Country A', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + }, + { + name: 'Country B', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + }, + { + name: 'Country C', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + } + ]; + const spec = { + data: [{ id: 'data', values: data }], + type: 'circlePacking', + categoryField: 'name', + valueField: 'value', + drill: true, + circlePacking: { style: { fillOpacity: d => (d.isLeaf ? 0.75 : 0.25) } }, + layoutPadding: 5, + label: { + style: { + fontSize: 10, + visible: d => { + return d.depth === 1; + } + } + }, + animationEnter: { easing: 'cubicInOut' }, + animationExit: { easing: 'cubicInOut' }, + animationUpdate: { easing: 'cubicInOut' }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__drillEvents = []; + window.__hierarchyBefore = JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + ); + c.on('drill', e => window.__drillEvents.push({ type: e.value.type, path: [...e.value.path] })); + }); + const p = await interactionTarget(page, 'circlePacking', 1); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + await page.evaluate( + () => + (window.__hierarchyAfter = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + )) + ); + await page.mouse.click(790, 590); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const e = window.__drillEvents; + if ( + e?.length !== 2 || + !e[0].path.length || + e[1].path.length >= e[0].path.length || + window.__hierarchyBefore === window.__hierarchyAfter + ) + throw Error('层级下钻/回退没有改变路径与布局'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-rooted.mjs b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-rooted.mjs new file mode 100644 index 0000000000..50f3f8026e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-drill-rooted.mjs @@ -0,0 +1,258 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7ef552a1e9eec95f9e92 + * 验证目的:来源层级图下钻与空白回退的路径和实际布局。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { + name: 'root', + children: [ + { + name: 'Country A', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + }, + { + name: 'Country B', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + }, + { + name: 'Country C', + children: [ + { + name: '东北', + children: [ + { name: '办公用品', value: 824 }, + { name: '家具', value: 920 }, + { name: '电子设备', value: 936 } + ] + }, + { + name: '中南', + children: [ + { name: '办公用品', value: 1270 }, + { name: '家具', value: 1399 }, + { name: '电子设备', value: 1466 } + ] + }, + { + name: '华东', + children: [ + { name: '办公用品', value: 1408 }, + { name: '家具', value: 1676 }, + { name: '电子设备', value: 1559 } + ] + }, + { + name: '华北', + children: [ + { name: '办公用品', value: 745 }, + { name: '家具', value: 919 }, + { name: '电子设备', value: 781 } + ] + }, + { + name: '西北', + children: [ + { name: '办公用品', value: 267 }, + { name: '家具', value: 316 }, + { name: '电子设备', value: 230 } + ] + }, + { + name: '西南', + children: [ + { name: '办公用品', value: 347 }, + { name: '家具', value: 501 }, + { name: '电子设备', value: 453 } + ] + } + ] + } + ] + } + ]; + const spec = { + data: [{ id: 'data', values: data }], + type: 'circlePacking', + categoryField: 'name', + valueField: 'value', + drill: true, + circlePacking: { style: { fillOpacity: d => (d.isLeaf ? 0.75 : 0.25) } }, + layoutPadding: 5, + label: { + style: { + fontSize: 10, + visible: d => { + return d.depth === 1; + } + } + }, + animationEnter: { easing: 'cubicInOut' }, + animationExit: { easing: 'cubicInOut' }, + animationUpdate: { easing: 'cubicInOut' }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__drillEvents = []; + window.__hierarchyBefore = JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + ); + c.on('drill', e => window.__drillEvents.push({ type: e.value.type, path: [...e.value.path] })); + }); + const p = await interactionTarget(page, 'circlePacking', 1); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + await page.evaluate( + () => + (window.__hierarchyAfter = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + )) + ); + await page.mouse.click(790, 590); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const e = window.__drillEvents; + if ( + e?.length !== 2 || + !e[0].path.length || + e[1].path.length >= e[0].path.length || + window.__hierarchyBefore === window.__hierarchyAfter + ) + throw Error('层级下钻/回退没有改变路径与布局'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-padding.mjs b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-padding.mjs new file mode 100644 index 0000000000..afaa101c39 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/circle-packing/circle-packing-padding.mjs @@ -0,0 +1,393 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ef552a1e9eec95f9e94 + * 验证目的:圆打包分层留白和按深度显示标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const data = [ + { + name: 'root', + children: [ + { + name: 'Country A', + children: [ + { + name: 'Northeast', + children: [ + { + name: 'Office supplies', + value: 824 + }, + { + name: 'Furniture', + value: 920 + }, + { + name: 'Electronics', + value: 936 + } + ] + }, + { + name: 'Central South', + children: [ + { + name: 'Office supplies', + value: 1270 + }, + { + name: 'Furniture', + value: 1399 + }, + { + name: 'Electronics', + value: 1466 + } + ] + }, + { + name: 'East', + children: [ + { + name: 'Office supplies', + value: 1408 + }, + { + name: 'Furniture', + value: 1676 + }, + { + name: 'Electronics', + value: 1559 + } + ] + }, + { + name: 'North', + children: [ + { + name: 'Office supplies', + value: 745 + }, + { + name: 'Furniture', + value: 919 + }, + { + name: 'Electronics', + value: 781 + } + ] + }, + { + name: 'Northwest', + children: [ + { + name: 'Office supplies', + value: 267 + }, + { + name: 'Furniture', + value: 316 + }, + { + name: 'Electronics', + value: 230 + } + ] + }, + { + name: 'Southwest', + children: [ + { + name: 'Office supplies', + value: 347 + }, + { + name: 'Furniture', + value: 501 + }, + { + name: 'Electronics', + value: 453 + } + ] + } + ] + }, + { + name: 'Country B', + children: [ + { + name: 'Northeast', + children: [ + { + name: 'Office supplies', + value: 824 + }, + { + name: 'Furniture', + value: 920 + }, + { + name: 'Electronics', + value: 936 + } + ] + }, + { + name: 'Central South', + children: [ + { + name: 'Office supplies', + value: 1270 + }, + { + name: 'Furniture', + value: 1399 + }, + { + name: 'Electronics', + value: 1466 + } + ] + }, + { + name: 'East', + children: [ + { + name: 'Office supplies', + value: 1408 + }, + { + name: 'Furniture', + value: 1676 + }, + { + name: 'Electronics', + value: 1559 + } + ] + }, + { + name: 'North', + children: [ + { + name: 'Office supplies', + value: 745 + }, + { + name: 'Furniture', + value: 919 + }, + { + name: 'Electronics', + value: 781 + } + ] + }, + { + name: 'Northwest', + children: [ + { + name: 'Office supplies', + value: 267 + }, + { + name: 'Furniture', + value: 316 + }, + { + name: 'Electronics', + value: 230 + } + ] + }, + { + name: 'Southwest', + children: [ + { + name: 'Office supplies', + value: 347 + }, + { + name: 'Furniture', + value: 501 + }, + { + name: 'Electronics', + value: 453 + } + ] + } + ] + }, + { + name: 'Country C', + children: [ + { + name: 'Northeast', + children: [ + { + name: 'Office supplies', + value: 824 + }, + { + name: 'Furniture', + value: 920 + }, + { + name: 'Electronics', + value: 936 + } + ] + }, + { + name: 'Central South', + children: [ + { + name: 'Office supplies', + value: 1270 + }, + { + name: 'Furniture', + value: 1399 + }, + { + name: 'Electronics', + value: 1466 + } + ] + }, + { + name: 'East', + children: [ + { + name: 'Office supplies', + value: 1408 + }, + { + name: 'Furniture', + value: 1676 + }, + { + name: 'Electronics', + value: 1559 + } + ] + }, + { + name: 'North', + children: [ + { + name: 'Office supplies', + value: 745 + }, + { + name: 'Furniture', + value: 919 + }, + { + name: 'Electronics', + value: 781 + } + ] + }, + { + name: 'Northwest', + children: [ + { + name: 'Office supplies', + value: 267 + }, + { + name: 'Furniture', + value: 316 + }, + { + name: 'Electronics', + value: 230 + } + ] + }, + { + name: 'Southwest', + children: [ + { + name: 'Office supplies', + value: 347 + }, + { + name: 'Furniture', + value: 501 + }, + { + name: 'Electronics', + value: 453 + } + ] + } + ] + } + ] + } + ]; + + const spec = { + data: [ + { + id: 'data', + values: data + } + ], + type: 'circlePacking', + categoryField: 'name', + valueField: 'value', + drill: true, + circlePacking: { + style: { + fillOpacity: d => (d.isLeaf ? 0.75 : 0.25) + } + }, + layoutPadding: [0, 10, 10], + label: { + style: { + fontSize: 10, + visible: d => { + return d.depth === 1; + } + } + }, + animationEnter: { + easing: 'cubicInOut' + }, + animationExit: { + easing: 'cubicInOut' + }, + animationUpdate: { + easing: 'cubicInOut' + }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/combination/bar-area-smooth.mjs b/packages/vchart/__tests__/visual/cases/charts/combination/bar-area-smooth.mjs new file mode 100644 index 0000000000..42dfb9bb07 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/combination/bar-area-smooth.mjs @@ -0,0 +1,110 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0393 + * 迁移方式:原配置迁移。 + * 验证目的:同一区域的柱图与平滑轮廓面积图叠加。 + * 保留条件:两个独立数据源、两个非堆叠系列、600×600、area.line 的 monotone 配置。 + * 覆盖边界:不改为单面积图,也不改变两份数据的对应关系;不宣称复现未提供的历史缺陷。 + */ +export default { + createSpec() { + // 返回原始配置结构的新副本,只移除宿主生命周期依赖。 + return { + type: 'common', + data: [ + { + id: 'id0', + values: [ + { + x: 1, + y: 20 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 30 + }, + { + x: 4, + y: 50 + }, + { + x: 5, + y: 40 + } + ] + }, + { + id: 'id1', + values: [ + { + x: 1, + y: 20 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 30 + }, + { + x: 4, + y: 50 + }, + { + x: 5, + y: 40 + } + ] + } + ], + width: 600, + height: 600, + region: [{}], + series: [ + { + stack: false, + type: 'bar', + dataIndex: 0, + xField: 'x', + yField: 'y' + }, + { + stack: false, + type: 'area', + dataIndex: 1, + xField: 'x', + yField: 'y', + line: { + style: { + curveType: 'monotone' + } + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom' + } + ] + }; + }, + async verify(page) { + // 精确核对所有原始数据、关联和开关,并检查真实系列状态。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries(); + if (s.length !== 2 || s[0].type !== 'bar' || s[1].type !== 'area') throw new Error('柱与面积系列组合丢失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/combination/combination-line-pie.mjs b/packages/vchart/__tests__/visual/cases/charts/combination/combination-line-pie.mjs new file mode 100644 index 0000000000..b8a64fc014 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/combination/combination-line-pie.mjs @@ -0,0 +1,145 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7ef752a1e9eec95f9e9a + * 验证目的:折线和饼图在独立区域共存。 + * 保留条件:padding, layout, region, legends, series, axes, tooltip;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + padding: { + top: 10 + }, + layout: { + type: 'grid', + col: 2, + row: 4, + elements: [ + { + modelId: 'legend', + col: 0, + colSpan: 2, + row: 0 + }, + { + modelId: 'pie-region', + col: 0, + colSpan: 2, + row: 1 + }, + { + modelId: 'axis-left', + col: 0, + row: 2 + }, + { + modelId: 'line-region', + col: 1, + row: 2 + }, + { + modelId: 'axis-bottom', + col: 1, + row: 3 + } + ] + }, + region: [ + { + id: 'pie-region', + height: '40%' + }, + { + id: 'line-region' + } + ], + legends: { + visible: true, + orient: 'top', + id: 'legend', + regionId: ['pie-region', 'line-region'] + }, + series: [ + { + regionId: 'pie-region', + type: 'pie', + valueField: 'value', + categoryField: 'type', + data: { + id: 'pie', + values: [ + { + type: 'a', + value: 10 + }, + { + type: 'b', + value: 20 + } + ] + }, + seriesField: 'type' + }, + { + regionId: 'line-region', + type: 'line', + xField: 'x', + yField: 'y', + data: { + id: 'line', + values: [ + { + x: '1', + y: 10, + type: 'a' + }, + { + x: '1', + y: 20, + type: 'b' + }, + { + x: '2', + y: 30, + type: 'a' + }, + { + x: '2', + y: 40, + type: 'b' + } + ] + }, + seriesField: 'type' + } + ], + axes: [ + { + id: 'axis-left', + regionId: 'line-region', + orient: 'left' + }, + { + id: 'axis-bottom', + regionId: 'line-region', + orient: 'bottom' + } + ], + tooltip: { + dimension: { + visible: true + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/correlation/correlation-tooltip.mjs b/packages/vchart/__tests__/visual/cases/charts/correlation/correlation-tooltip.mjs new file mode 100644 index 0000000000..da871bedb3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/correlation/correlation-tooltip.mjs @@ -0,0 +1,264 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6530ea3a8632117ed798a239 + * 验证目的:关联图布局及源移动鼠标后的 tooltip。 + * 保留条件:三十条数据的数值、顺序、size/value 字段映射和半径。 + * 迁移说明:公开示例搜索词统一匿名化为 Input method 序号;移动鼠标按实际首个节点定位。 + * 覆盖边界:本地选择首个节点,不宣称该节点是原录制坐标目标;不代表所有节点与状态组合。 + */ +export default { + createSpec() { + // 保留原始关联数据与大小编码,每次返回独立配置。 + return { + type: 'correlation', + data: [ + { + values: [ + { + word: 'Input method 1', + pv: 15952, + ratio: 94, + sim: 3932 + }, + { + word: 'Input method 2', + pv: 11032, + ratio: 97, + sim: 2799 + }, + { + word: 'Input method 3', + pv: 107908, + ratio: 102, + sim: 2645 + }, + { + word: 'Input method 4', + pv: 74912, + ratio: 99, + sim: 2189 + }, + { + word: 'Input method 5', + pv: 193624, + ratio: 121, + sim: 2100 + }, + { + word: 'Input method 6', + pv: 835168, + ratio: 88, + sim: 2050 + }, + { + word: 'Input method 7', + pv: 14140, + ratio: 96, + sim: 1953 + }, + { + word: 'Input method 8', + pv: 19236, + ratio: 97, + sim: 1870 + }, + { + word: 'Input method 9', + pv: 1968, + ratio: 109, + sim: 1705 + }, + { + word: 'Input method 10', + pv: 812, + ratio: 150, + sim: 1567 + }, + { + word: 'Input method 11', + pv: 4602, + ratio: 91, + sim: 1522 + }, + { + word: 'Input method 12', + pv: 18262, + ratio: 97, + sim: 1486 + }, + { + word: 'Input method 13', + pv: 34186, + ratio: 91, + sim: 1278 + }, + { + word: 'Input method 14', + pv: 7186, + ratio: 86, + sim: 1009 + }, + { + word: 'Input method 15', + pv: 13418, + ratio: 102, + sim: 924 + }, + { + word: 'Input method 16', + pv: 4680, + ratio: 88, + sim: 804 + }, + { + word: 'Input method 17', + pv: 2206, + ratio: 97, + sim: 800 + }, + { + word: 'Input method 18', + pv: 15112, + ratio: 85, + sim: 764 + }, + { + word: 'Input method 19', + pv: 8204, + ratio: 135, + sim: 754 + }, + { + word: 'Input method 20', + pv: 5590, + ratio: 106, + sim: 609 + }, + { + word: 'Input method 21', + pv: 352, + ratio: 132, + sim: 593 + }, + { + word: 'Input method 22', + pv: 2476, + ratio: 103, + sim: 540 + }, + { + word: 'Input method 23', + pv: 1582, + ratio: 86, + sim: 538 + }, + { + word: 'Input method 24', + pv: 1298, + ratio: 75, + sim: 527 + }, + { + word: 'Input method 25', + pv: 126182, + ratio: 102, + sim: 521 + }, + { + word: 'Input method 26', + pv: 3442, + ratio: 88, + sim: 510 + }, + { + word: 'Input method 27', + pv: 24912, + ratio: 98, + sim: 478 + }, + { + word: 'Input method 28', + pv: 150, + ratio: 125, + sim: 465 + }, + { + word: 'Input method 29', + pv: 264, + ratio: 89, + sim: 452 + }, + { + word: 'Input method 30', + pv: 2772, + ratio: 93, + sim: 443 + } + ] + } + ], + categoryField: 'word', + valueField: 'sim', + sizeField: 'pv', + sizeRange: [12, 30], + innerRadius: '25%', + outerRadius: '95%', + nodePoint: { + state: { + hover: { + lineWidth: 8, + strokeOpacity: 0.2 + } + } + }, + centerPoint: { + state: { + hover: { + lineWidth: 8, + strokeOpacity: 0.2 + } + } + }, + centerLabel: { + visible: true, + position: 'center', + style: { + fill: 'white', + text: 'Input methods' + } + }, + label: { + visible: true, + position: 'bottom', + style: { + fill: 'black' + } + } + }; + }, + async exercise(page) { + // 来源记录为移动鼠标;根据节点几何适配当前宿主位置。 + const point = await page.evaluate(() => { + const g = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics()[0]; + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(point.x, point.y); + }, + async verify(page) { + // 核对完整输入、实际布局以及目标节点 tooltip 文本。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => + [...document.querySelectorAll('[class*="tooltip"]')].some( + el => + el.textContent.includes('Input method 1') && + el.textContent.includes('3932') && + el.getBoundingClientRect().width > 0 && + getComputedStyle(el).visibility !== 'hidden' && + getComputedStyle(el).display !== 'none' + ) + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-left.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-left.mjs new file mode 100644 index 0000000000..ced349a480 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-left.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03eb + * 验证目的:下向漏斗左对齐。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'left', + funnelOrient: 'bottom', + funnel: { + style: { + stroke: 'red', + lineWidth: 1 + } + }, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: false + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue Revenue Revenue' + }, + { + value: 80, + name: 'Impressions Impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Inquiries Inquiries' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-right.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-right.mjs new file mode 100644 index 0000000000..0b4ecf2ff0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-bottom-align-right.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03ec + * 验证目的:下向漏斗右对齐。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'right', + funnelOrient: 'bottom', + funnel: { + style: { + stroke: 'red', + lineWidth: 1 + } + }, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: false + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue Revenue Revenue' + }, + { + value: 80, + name: 'Impressions Impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Inquiries Inquiries' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-cone-transform.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-cone-transform.mjs new file mode 100644 index 0000000000..f59dc88885 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-cone-transform.mjs @@ -0,0 +1,66 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03ee + * 验证目的:锥形漏斗与转化区域标签。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'funnel', + isCone: true, + animation: false, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: true + }, + transformLabel: { + visible: true + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue Revenue Revenue' + }, + { + value: 80, + name: 'Impressions Impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Inquiries Inquiries' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-directions.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-directions.mjs new file mode 100644 index 0000000000..47dfcc20a0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-directions.mjs @@ -0,0 +1,207 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0052a1e9eec95f9eb0 + * 验证目的:四区域漏斗的方向、对齐与外侧标签。 + * 保留条件:padding, region, series, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + padding: 0, + data: [ + { + id: 'funnel1', + values: [ + { + value: 100, + name: 'Step1' + }, + { + value: 80, + name: 'Step2' + }, + { + value: 60, + name: 'Step3' + }, + { + value: 40, + name: 'Step4' + }, + { + value: 20, + name: 'Step5' + } + ] + }, + { + name: 'funnel2', + values: [ + { + value: 200, + name: 'Step1' + }, + { + value: 160, + name: 'Step2' + }, + { + value: 120, + name: 'Step3' + }, + { + value: 80, + name: 'Step4' + }, + { + value: 40, + name: 'Step5' + } + ] + } + ], + region: [ + { + width: '25%' + }, + { + width: '25%', + offsetX: '25%', + padding: { + left: 1 + } + }, + { + height: '45%', + width: '50%', + offsetX: '50%' + }, + { + height: '45%', + width: '50%', + offsetX: '50%', + offsetY: '45%', + padding: { + top: 1 + } + } + ], + series: [ + { + type: 'funnel', + dataIndex: 0, + gap: 2, + range: { + max: 200 + }, + funnelOrient: 'top', + funnelAlign: 'right', + categoryField: 'name', + valueField: 'value', + isCone: false, + funnel: { + style: { + cornerRadius: 4 + } + }, + label: { + visible: true, + style: { + text: '' + } + }, + outerLabel: { + visible: true, + alignLabel: false, + position: 'left' + } + }, + { + type: 'funnel', + regionIndex: 1, + dataIndex: 1, + gap: 2, + funnelOrient: 'top', + funnelAlign: 'left', + categoryField: 'name', + valueField: 'value', + isCone: false, + label: { + visible: true, + style: { + text: '' + } + }, + outerLabel: { + visible: true, + alignLabel: false, + position: 'right' + }, + funnel: { + style: { + cornerRadius: 4 + } + } + }, + { + type: 'funnel', + dataIndex: 0, + regionIndex: 2, + gap: 1, + funnelOrient: 'top', + funnelAlign: 'left', + categoryField: 'name', + valueField: 'value', + isCone: false, + label: { + visible: true, + style: { + text: '' + } + }, + outerLabel: { + visible: true, + alignLabel: false, + position: 'right' + } + }, + { + type: 'funnel', + regionIndex: 3, + dataIndex: 1, + gap: 1, + funnelOrient: 'bottom', + funnelAlign: 'left', + categoryField: 'name', + valueField: 'value', + isCone: false, + label: { + visible: true, + style: { + text: '' + } + }, + outerLabel: { + visible: true, + alignLabel: false, + position: 'right' + } + } + ], + legends: { + visible: true, + orient: 'top' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-extension-select.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-extension-select.mjs new file mode 100644 index 0000000000..a2c5441463 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-extension-select.mjs @@ -0,0 +1,341 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 660145d53b10c900bdd7d4a5 + * 验证目的:来源漏斗扩展图元、初始选择和后续点击选择切换。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + color: { type: 'ordinal', range: ['#148FFA', '#2DA8FA', '#46BEFA', '#5ACAFA', '#78D7FA', '#96DCFA'] }, + select: { triggerOff: 'none', markIds: ['mainFunnel', 'backgroundRect', 'outerLabel'] }, + interactions: [ + { + type: 'element-highlight-by-key', + highlightState: 'selected', + trigger: 'pointertap', + markIds: ['mainFunnel', 'backgroundRect', 'outerLabel'], + triggerOff: null + } + ], + data: [ + { + name: 'funnel', + id: 'funnel', + values: [ + { value: 100, name: '曝光', percent: 1, ratio: 0.8 }, + { value: 80, name: '看播', percent: 0.8, ratio: 0.75 }, + { value: 60, name: '有效看播', percent: 0.75, ratio: 0.67 }, + { value: 50, name: '有效看播1', percent: 0.45, ratio: 0.57 }, + { value: 40, name: '组件点击', percent: 0.67, ratio: 0.75 }, + { value: 30, name: '留资', percent: 0.75 } + ] + } + ], + padding: { left: '40%', bottom: 4, top: 4 }, + series: [ + { + type: 'funnel', + maxSize: '65%', + minSize: '10%', + funnelAlign: 'left', + gap: 12, + funnel: { id: 'mainFunnel', style: { cornerRadius: 4, zIndex: 100, stroke: 'white', lineWidth: 1 } }, + label: { + visible: true, + smartInvert: true, + style: { text: datum => `${datum.name}`, textAlign: 'left', x: 0, dx: 10 } + }, + outerLabel: { + id: 'outerLabel', + visible: true, + position: 'right', + formatMethod: (v, datum) => datum.value, + style: { + pickable: false, + fontSize: 18, + dx: -20 + }, + state: { + selected: { + fill: 'blue' + } + }, + line: { + visible: false + } + }, + extensionMark: [ + { + type: 'polygon', + dataId: 'funnel', + style: { + points: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return; + } + console.log(ctx); + const nextDatum = data[curIndex + 1]; + const firstDatum = data[0]; + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum); + const firstPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(firstDatum); + const tl = points[0]; + const bl = points[3]; + const next_tl = nextPoints[0]; + const next_bl = nextPoints[3]; + const first_tl = firstPoints[0]; + const first_bl = firstPoints[3]; + const result = [ + { x: tl.x - 20, y: (tl.y + bl.y) / 2 }, + { x: first_tl.x - 100, y: (tl.y + bl.y) / 2 }, + { + x: first_tl.x - 100, + y: (tl.y + bl.y) / 2 + (next_tl.y - tl.y) - 13 + }, + { + x: next_tl.x - 20, + y: (tl.y + bl.y) / 2 + (next_tl.y - tl.y) - 13 + } + ]; + return result; + }, + cornerRadius: 6, + stroke: '#E1E4E8', + strokeOpacity: 1, + lineWidth: 2, + closePath: false + } + }, + { + type: 'symbol', + dataId: 'funnel', + style: { + visible: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return false; + } + return true; + }, + x: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return; + } + const nextDatum = data[curIndex + 1]; + const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum); + const next_tl = nextPoints[0]; + return next_tl.x - 20; + }, + y: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return; + } + const nextDatum = data[curIndex + 1]; + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum); + const tr = points[1]; + const br = points[2]; + const next_tr = nextPoints[1]; + const next_br = nextPoints[2]; + return (tr.y + br.y) / 2 + (next_tr.y - tr.y) - 13; + }, + size: 12, + scaleX: 0.8, + symbolType: 'triangleRight', + cornerRadius: 2, + fill: '#E1E4E8' + } + }, + { + type: 'text', + dataId: 'funnel', + style: { + text: datum => { + const fill = datum.ratio < 0.75 ? 'orange' : 'green'; + const image = + datum.ratio < 0.75 + ? '' + : ''; + return { + type: 'rich', + text: [ + { + text: `${(datum.ratio * 100).toFixed(2)}% `, + fontSize: 14, + fontWeight: 'bold', + fill, + textAlign: 'right' + }, + { + image, + width: 15, + height: 15, + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green' + }, + { + text: `\n \n周边同行均值 72.80%`, + fontSize: 14, + fill: 'grey' + } + ] + }; + }, + textAlign: 'right', + verticalDirection: 'middle', + visible: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return false; + } + return true; + }, + x: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const firstDatum = data[0]; + const firstPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(firstDatum); + const tl = firstPoints[0]; + return tl.x - 100 - 30; + }, + y: (datum, ctx, params, dataView) => { + const data = dataView.latestData; + if (!data) { + return; + } + const curIndex = data.findIndex(d => d.name === datum.name); + if (curIndex === data.length - 1) { + return; + } + const nextDatum = data[curIndex + 1]; + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const nextPoints = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(nextDatum); + const tr = points[1]; + const br = points[2]; + const next_tr = nextPoints[1]; + return ((tr.y + br.y) / 2 + (next_tr.y - tr.y) - 10 + (tr.y + br.y) / 2) / 2; + }, + fontSize: 12, + fill: 'black' + } + }, + { + type: 'rect', + dataId: 'funnel', + id: 'backgroundRect', + zIndex: 0, + state: { + selected: { + outerBorder: { + stroke: '#0088FF', + lineWidth: 1, + strokeOpacity: 1, + distance: 2 + }, + fill: '#F0FBFF', + fillOpacity: 1 + } + }, + style: { + x: (datum, ctx, params, dataView) => { + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const tl = points[0]; + return tl.x; + }, + y: (datum, ctx, params, dataView) => { + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const tl = points[0]; + const bl = points[3]; + return tl.y; + }, + width: (datum, ctx, params, dataView) => { + const region = ctx.getRegion(); + const { width, height } = region.getLayoutRect(); + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const tl = points[0]; + return width - tl.x - 10; + }, + height: (datum, ctx, params, dataView) => { + const region = ctx.getRegion(); + const { width, height } = region.getLayoutRect(); + const points = ctx.vchart.getChart().getSeriesInIndex(0)[0].getPoints(datum); + const tl = points[0]; + const bl = points[3]; + return bl.y - tl.y; + }, + zIndex: 10, + cornerRadius: 5, + fill: '#F7F9FC' + } + } + ], + categoryField: 'name', + valueField: 'value' + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => window.__visualChart.setSelected([{ name: '曝光' }])); + await interactionFrame(page); + const initial = await seriesStates(page); + const p = await interactionTarget(page, 'funnel', 1); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(v => (window.__funnelInitial = v), initial); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + s = c.getChart().getAllSeries()[0]; + if (!window.__funnelInitial?.some(m => m.states.selected > 0)) throw Error('来源初始 setSelected 未生效'); + const selected = s + .getSeriesMark() + .getGraphics() + .filter(g => g.currentStates?.includes('selected')); + if (!selected.length || selected.every(g => g.context.data[0].name === '曝光')) + throw Error('点击没有切换漏斗选择'); + if (!s.getMarks().some(m => m.name.includes('extensionMark') && m.getGraphics().length)) + throw Error('扩展图元未绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-bottom.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-bottom.mjs new file mode 100644 index 0000000000..ba8f5c8ec1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-bottom.mjs @@ -0,0 +1,77 @@ +import { verifySpec, verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03e8 + * 验证目的:左向漏斗底部对齐与外标签。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const spec = { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'bottom', + funnelOrient: 'left', + funnel: { + style: { + stroke: 'red', + lineWidth: 1 + } + }, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: false + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue with a very long label' + }, + { + value: 80, + name: 'Long impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Long consultations' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + + return spec; + }, + async verify(page) { + // 验证目标状态及实际绘制,动作缺失不能通过。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-top.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-top.mjs new file mode 100644 index 0000000000..cd9c548bc1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-left-align-top.mjs @@ -0,0 +1,81 @@ +import { verifySpec, verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03e7 + * 验证目的:左向漏斗顶部对齐与外标签。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const spec = { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'top', + funnelOrient: 'left', + funnel: { + style: { + stroke: 'red', + strokeWidth: 1 + } + }, + label: { + visible: true, + style: {} + }, + outerLabel: { + visible: true, + alignLabel: false, + style: { + limit: Infinity + } + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue with a very long label' + }, + { + value: 80, + name: 'Long impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Long consultations' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + + return spec; + }, + async verify(page) { + // 验证目标状态及实际绘制,动作缺失不能通过。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-outer-label-line.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-outer-label-line.mjs new file mode 100644 index 0000000000..16fc0a9a5f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-outer-label-line.mjs @@ -0,0 +1,113 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 66f2a5a8885776011c598a43 + * 验证目的:来源漏斗外标签对齐、虚线与悬停。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'funnel', + maxSize: '75%', + minSize: '10%', + isTransform: true, + shape: 'rect', + color: { + type: 'ordinal', + range: ['#00328E', '#0048AA', '#005FC5', '#2778E2', '#4E91FF', '#70ABFF', '#8FC7FF', '#AEE2FF'] + }, + transformRatioText: 'transformRatio', + funnel: { + style: { cornerRadius: 4, stroke: 'white', lineWidth: 2 }, + state: { hover: { stroke: '#4e83fd', lineWidth: 1 } } + }, + transform: { style: { stroke: 'white', lineWidth: 2 }, state: { hover: { stroke: '#4e83fd', lineWidth: 1 } } }, + label: { + visible: true, + style: { lineHeight: 16, limit: Infinity, text: datum => [`${datum.name}`, `${datum.value}`] } + }, + outerLabel: { + visible: true, + alignLabel: true, + style: { + text: datum => { + return `${datum.name}`; + } + }, + line: { + minLength: 0, + style: { + lineDash: [2, 2] + } + } + }, + transformLabel: { + visible: true, + style: { + fill: 'black' + } + }, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Resume Screening', + percent: 1 + }, + { + value: 80, + name: 'Resume Evaluation', + percent: 0.8 + }, + { + value: 50, + name: 'Evaluation Passed', + percent: 0.5 + }, + { + value: 30, + name: 'Interview', + percent: 0.3 + }, + { + value: 10, + name: 'Final Pass', + percent: 0.1 + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'funnel'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(r => (window.__funnelHover = r), await seriesStates(page)); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + if (!window.__funnelHover?.some(m => m.name === 'funnel' && m.states.hover > 0)) throw Error('漏斗未悬停'); + const lines = s + .getMarks() + .find(m => m.name === 'outerLabelLine') + .getGraphics(); + if (lines.length !== 5 || !lines.every(g => JSON.stringify(g.attribute.lineDash) === '[2,2]')) + throw Error('外标签虚线未绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-bottom.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-bottom.mjs new file mode 100644 index 0000000000..aaad453950 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-bottom.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03ea + * 验证目的:右向漏斗底部对齐与长外标签。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'bottom', + funnelOrient: 'right', + funnel: { + style: { + stroke: 'red', + strokeWidth: 1 + } + }, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: false + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue Revenue Revenue' + }, + { + value: 80, + name: 'Impressions Impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Inquiries Inquiries' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-top.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-top.mjs new file mode 100644 index 0000000000..b9ed361bb4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-right-align-top.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03e9 + * 验证目的:右向漏斗顶部对齐与长外标签。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'funnel', + isCone: false, + gap: 10, + animation: false, + funnelAlign: 'top', + funnelOrient: 'right', + funnel: { + style: { + stroke: 'red', + strokeWidth: 1 + } + }, + label: { + visible: true + }, + outerLabel: { + visible: true, + alignLabel: false + }, + transformLabel: { + visible: false + }, + isTransform: true, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Revenue Revenue Revenue' + }, + { + value: 80, + name: 'Impressions Impressions' + }, + { + value: 50, + name: 'Clicks' + }, + { + value: 30, + name: 'Visits' + }, + { + value: 10, + name: 'Inquiries Inquiries' + }, + { + value: 5, + name: 'Orders' + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-transform.mjs b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-transform.mjs new file mode 100644 index 0000000000..d5403f7575 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/funnel/funnel-transform.mjs @@ -0,0 +1,71 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0152a1e9eec95f9eb2 + * 验证目的:转化漏斗的转化标签与外侧标签。 + * 保留条件:categoryField, valueField, isTransform, isCone, title, label, transformLabel, outerLabel, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'funnel', + categoryField: 'name', + valueField: 'value', + isTransform: true, + isCone: false, + data: [ + { + id: 'funnel', + values: [ + { + value: 5676, + name: 'Sent' + }, + { + value: 3872, + name: 'Viewed' + }, + { + value: 1668, + name: 'Clicked' + }, + { + value: 610, + name: 'Add to Cart' + }, + { + value: 565, + name: 'Purchased' + } + ] + } + ], + title: { + visible: true, + text: 'Percentage of the customers have dropped from the sales process' + }, + label: { + visible: true + }, + transformLabel: { + visible: true + }, + outerLabel: { + position: 'right', + visible: true + }, + legends: { + visible: true, + orient: 'top' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-gradient.mjs b/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-gradient.mjs new file mode 100644 index 0000000000..8f448939b4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-gradient.mjs @@ -0,0 +1,87 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0152a1e9eec95f9eb5 + * 验证目的:仪表盘渐变圆弧和指针组件。 + * 保留条件:radiusField, angleField, seriesField, outerRadius, innerRadius, startAngle, endAngle, gauge, pointer, pin, pinBackground;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'gauge', + data: [ + { + id: 'id0', + values: [ + { + type: 'Target A', + value: 0.6 + } + ] + } + ], + radiusField: 'type', + angleField: 'value', + seriesField: 'type', + outerRadius: 0.8, + innerRadius: 0.5, + startAngle: -225, + endAngle: 45, + gauge: { + type: 'circularProgress', + progress: { + style: { + fill: { + gradient: 'conical', + stops: [ + { + offset: 0, + color: '#4FC6B4' + }, + { + offset: 1, + color: '#31679E' + } + ] + } + } + }, + track: { + style: { + fill: '#ccc' + } + } + }, + pointer: { + width: 0.5, + height: 0.5, + style: { + path: 'M-0.020059 -0.978425 C-0.018029 -0.9888053 -0.013378 -1 0 -1 C0.01342 -1 0.01812 -0.989146 0.0201 -0.978425 C0.02161 -0.9702819 0.0692 -0.459505 0.09486 -0.184807 C0.10298 -0.097849 0.1089 -0.034548 0.11047 -0.018339 C0.11698 0.04908 0.07373 0.11111 0.00002 0.11111 C-0.07369 0.11111 -0.117184 0.04991 -0.110423 -0.018339 C-0.103662 -0.086591 -0.022089 -0.9680447 -0.020059 -0.978425Z', + fill: '#5A595E' + } + }, + pin: { + style: { + path: 'M1 0 C1 0.55228 0.55228 1 0 1 C-0.552285 1 -1 0.55228 -1 0 C-1 -0.552285 -0.552285 -1 0 -1 C0.55228 -1 1 -0.552285 1 0Z', + fill: '#888' + } + }, + pinBackground: { + width: 0.08, + height: 0.08, + style: { + path: 'M1 0 C1 0.55228 0.55228 1 0 1 C-0.552285 1 -1 0.55228 -1 0 C-1 -0.552285 -0.552285 -1 0 -1 C0.55228 -1 1 -0.552285 1 0Z', + fill: '#ddd' + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-tick-segment.mjs b/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-tick-segment.mjs new file mode 100644 index 0000000000..556de7bde0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/gauge/gauge-tick-segment.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65030e7dce75480c0b15d21f + * 验证目的:仪表盘刻度分段和指针。 + * 保留条件:gauge, pointer, categoryField, valueField, outerRadius, innerRadius, startAngle, endAngle, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'gauge', + data: [ + { + id: 'pointer', + values: [ + { + type: 'A', + value: 0.6 + } + ] + }, + { + id: 'segment', + values: [ + { + type: 'level1', + value: 0.4 + }, + { + type: 'level2', + value: 0.6 + }, + { + type: 'level3', + value: 0.8 + } + ] + } + ], + gauge: { + type: 'gauge', + dataIndex: 1, + categoryField: 'type', + valueField: 'value', + seriesField: 'type', + tickMask: { + visible: true, + angle: 3, + offsetAngle: 0, + forceAlign: true, + style: { + cornerRadius: 15 + } + } + }, + pointer: { + style: { + fill: '#666666' + } + }, + categoryField: 'type', + valueField: 'value', + outerRadius: 0.8, + innerRadius: 0.5, + startAngle: -180, + endAngle: 0, + axes: [ + { + type: 'linear', + orient: 'angle', + grid: { + visible: false + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/heatmap/heatmap-correlation.mjs b/packages/vchart/__tests__/visual/cases/charts/heatmap/heatmap-correlation.mjs new file mode 100644 index 0000000000..4ab486eb40 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/heatmap/heatmap-correlation.mjs @@ -0,0 +1,149 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0352a1e9eec95f9ebb + * 验证目的:相关矩阵热力图与固定区域及旋转标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const items = [ + 'Debt to assets', + 'Debt to assets excluding advances', + 'Long term debt ratio', + 'Long term asset ratio', + 'Equity multiplier', + 'Current debt to equity', + 'Interest debt to capital', + 'Current debt share', + 'Fixed capital ratio', + 'Expected default frequency' + ]; + const rawData = [ + 1.0, 0.594527, 0.492963, -0.160995, 0.723664, 0.658646, -0.857474, 0.320706, -0.284634, -0.091423, 0.594527, 1.0, + 0.724546, -0.099318, 0.540639, 0.49214, -0.554039, 0.17127, -0.265259, 0.068577, 0.492963, 0.724546, 1.0, + -0.091338, 0.450542, 0.375839, -0.524955, 0.300627, -0.198362, 0.033209, -0.160995, -0.099318, -0.091338, 1.0, + -0.049872, -0.028452, 0.157157, 0.009742, -0.162374, 0.155095, 0.723664, 0.540639, 0.450542, -0.049872, 1.0, + 0.951933, -0.651767, 0.079052, -0.535984, 0.00798, 0.658646, 0.49214, 0.375839, -0.028452, 0.951933, 1.0, + -0.543147, -0.106139, -0.52232, 0.011466, -0.857474, -0.554039, -0.524955, 0.157157, -0.651767, -0.543147, 1.0, + -0.595016, 0.310521, 0.066397, 0.320706, 0.17127, 0.300627, 0.009742, 0.079052, -0.106139, -0.595016, 1.0, + -0.105199, -0.064886, -0.284634, -0.265259, -0.198362, -0.162374, -0.535984, -0.52232, 0.310521, -0.105199, 1.0, + -0.080153, -0.091423, 0.068577, 0.033209, 0.155095, 0.00798, 0.011466, 0.066397, -0.064886, -0.080153, 1.0 + ]; + const data = []; + for (let i = 0; i < items.length; i++) { + for (let j = 0; j < items.length; j++) { + data.push({ + var1: items[i], + var2: items[j], + value: rawData[i * items.length + j] + }); + } + } + + const spec = { + type: 'common', + padding: 12, + data: [ + { + id: 'data0', + values: data + } + ], + series: [ + { + type: 'heatmap', + regionId: 'region0', + xField: 'var1', + yField: 'var2', + valueField: 'value', + cell: { + style: { + fill: { + field: 'value', + scale: 'color' + } + } + } + } + ], + region: [ + { + id: 'region0', + width: 200, // 限定 region 的宽度 + height: 200 // 限定 region 的高度 + } + ], + color: { + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['value'] + } + ], + range: ['#feedde', '#fdbe85', '#fd8d3c', '#e6550d', '#a63603'] + }, + axes: [ + { + orient: 'bottom', + type: 'band', + grid: { + visible: false + }, + domainLine: { + visible: false + }, + label: { + space: 10, + style: { + textAlign: 'left', + textBaseline: 'middle', + angle: 90 + } + }, + bandPadding: 0, + height: layoutRect => { + // 将底部轴的高度设置为画布的高度 - region 高度 - paddingTop - paddingBottom + return layoutRect.height - 224; + } + }, + { + orient: 'left', + type: 'band', + grid: { + visible: false + }, + domainLine: { + visible: false + }, + maxWidth: '50%', + label: { + autoLimit: false, + space: 10 + }, + bandPadding: 0 + } + ], + legends: { + visible: false, + orient: 'right', + type: 'color', + field: 'value', + title: { + visible: true, + text: `Correlation Coefficient` + } + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/histogram/histogram-min-height.mjs b/packages/vchart/__tests__/visual/cases/charts/histogram/histogram-min-height.mjs new file mode 100644 index 0000000000..8fc48155ed --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/histogram/histogram-min-height.mjs @@ -0,0 +1,319 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 650481368f73d6fa71c7eed0 + * 验证目的:直方图区间边界与最小柱高。 + * 保留条件:xField, x2Field, yField, bar, barMinHeight;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + data: [ + { + name: 'data1', + values: [ + { + x0: -400, + x1: -380, + frequency: 0 + }, + { + x0: -380, + x1: -360, + frequency: 4 + }, + { + x0: -360, + x1: -340, + frequency: 7 + }, + { + x0: -340, + x1: -320, + frequency: 7 + }, + { + x0: -320, + x1: -300, + frequency: 18 + }, + { + x0: -300, + x1: -280, + frequency: 30 + }, + { + x0: -280, + x1: -260, + frequency: 33 + }, + { + x0: -260, + x1: -240, + frequency: 80 + }, + { + x0: -240, + x1: -220, + frequency: 98 + }, + { + x0: -220, + x1: -200, + frequency: 124 + }, + { + x0: -200, + x1: -180, + frequency: 161 + }, + { + x0: -180, + x1: -160, + frequency: 176 + }, + { + x0: -160, + x1: -140, + frequency: 227 + }, + { + x0: -140, + x1: -120, + frequency: 276 + }, + { + x0: -120, + x1: -100, + frequency: 321 + }, + { + x0: -100, + x1: -80, + frequency: 452 + }, + { + x0: -80, + x1: -60, + frequency: 441 + }, + { + x0: -60, + x1: -40, + frequency: 505 + }, + { + x0: -40, + x1: -20, + frequency: 521 + }, + { + x0: -20, + x1: 0, + frequency: 733 + }, + { + x0: 0, + x1: 20, + frequency: 892 + }, + { + x0: 20, + x1: 40, + frequency: 362 + }, + { + x0: 40, + x1: 60, + frequency: 267 + }, + { + x0: 60, + x1: 80, + frequency: 223 + }, + { + x0: 80, + x1: 100, + frequency: 157 + }, + { + x0: 100, + x1: 120, + frequency: 170 + }, + { + x0: 120, + x1: 140, + frequency: 124 + }, + { + x0: 140, + x1: 160, + frequency: 112 + }, + { + x0: 160, + x1: 180, + frequency: 73 + }, + { + x0: 180, + x1: 200, + frequency: 80 + }, + { + x0: 200, + x1: 220, + frequency: 49 + }, + { + x0: 220, + x1: 240, + frequency: 33 + }, + { + x0: 240, + x1: 260, + frequency: 30 + }, + { + x0: 260, + x1: 280, + frequency: 21 + }, + { + x0: 280, + x1: 300, + frequency: 9 + }, + { + x0: 300, + x1: 320, + frequency: 13 + }, + { + x0: 320, + x1: 340, + frequency: 11 + }, + { + x0: 340, + x1: 360, + frequency: 5 + }, + { + x0: 360, + x1: 380, + frequency: 4 + }, + { + x0: 380, + x1: 400, + frequency: 4 + }, + { + x0: 400, + x1: 420, + frequency: 2 + }, + { + x0: 420, + x1: 440, + frequency: 8 + }, + { + x0: 440, + x1: 460, + frequency: 2 + }, + { + x0: 460, + x1: 480, + frequency: 3 + }, + { + x0: 480, + x1: 500, + frequency: 10 + }, + { + x0: 500, + x1: 520, + frequency: 7 + }, + { + x0: 520, + x1: 540, + frequency: 14 + }, + { + x0: 540, + x1: 560, + frequency: 6 + }, + { + x0: 560, + x1: 580, + frequency: 1 + }, + { + x0: 580, + x1: 600, + frequency: 3 + }, + { + x0: 600, + x1: 620, + frequency: 0 + }, + { + x0: 620, + x1: 640, + frequency: 6 + }, + { + x0: 640, + x1: 660, + frequency: 5 + }, + { + x0: 660, + x1: 680, + frequency: 3 + }, + { + x0: 680, + x1: 700, + frequency: 2 + }, + { + x0: 700, + x1: 720, + frequency: 0 + } + ] + } + ], + type: 'histogram', + xField: 'x0', + x2Field: 'x1', + yField: 'frequency', + bar: { + style: { + stroke: 'white', + lineWidth: 1 + } + }, + barMinHeight: 5 + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-band-invalid-zero.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-band-invalid-zero.mjs new file mode 100644 index 0000000000..4877316828 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-band-invalid-zero.mjs @@ -0,0 +1,60 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e037d + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:双离散轴折线保留 invalidType=zero 配置。 + * 保留条件:common、双 band 轴、字母 y 值、600×600;原文没有缺失数据。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'common', + autoFit: true, + data: [ + { + id: 'id0', + values: [ + { + x: 1, + y: 'a' + }, + { + x: 2, + y: 'b' + } + ] + } + ], + width: 600, + height: 600, + series: [ + { + type: 'line', + dataIndex: 0, + xField: 'x', + yField: 'y', + invalidType: 'zero' + } + ], + axes: [ + { + orient: 'left', + type: 'band' + }, + { + orient: 'bottom', + type: 'band' + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-gap.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-gap.mjs new file mode 100644 index 0000000000..cce3eaf6bd --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-gap.mjs @@ -0,0 +1,28 @@ +import { verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:缺失值与折线连接(data-zoom-brush-line)。 + * 图表类型:line。 + * 关键配置:invalidType、line.style、point.visible。 + * 场景条件:两个有效值之间包含 null,invalidType 固定为 break。 + * 最终检查:核对断点 spec、有效绘制与最终截图。 + * 覆盖边界:不验证连接缺失值或其他插值策略。 + */ +export default { + createSpec() { + // 使用空值验证断点,保留标记点便于发现数据错位。 + return { + type: 'line', + data: { id: 'data', values: [12, 28, null, 18, 42].map((y, x) => ({ x: String(x), y })) }, + xField: 'x', + yField: 'y', + line: { style: { lineWidth: 3 } }, + point: { visible: true }, + invalidType: 'break' + }; + }, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-missing-link.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-missing-link.mjs new file mode 100644 index 0000000000..444332fcf8 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-missing-link.mjs @@ -0,0 +1,271 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0b52a1e9eec95f9ed2 + * 验证目的:连接缺失值的折线与多系列数据。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'line', + data: { + values: [ + { + medalType: 'Gold Medals', + count: 40, + year: '1952' + }, + { + medalType: 'Gold Medals', + count: 32, + year: '1956' + }, + { + medalType: 'Gold Medals', + count: 34, + year: '1960' + }, + { + medalType: 'Gold Medals', + count: 36, + year: '1964' + }, + { + medalType: 'Gold Medals', + count: 45, + year: '1968' + }, + { + medalType: 'Gold Medals', + count: 33, + year: '1972' + }, + { + medalType: 'Gold Medals', + count: 34, + year: '1976' + }, + { + medalType: 'Gold Medals', + count: null, + year: '1980' + }, + { + medalType: 'Gold Medals', + count: 83, + year: '1984' + }, + { + medalType: 'Gold Medals', + count: 36, + year: '1988' + }, + { + medalType: 'Gold Medals', + count: 37, + year: '1992' + }, + { + medalType: 'Gold Medals', + count: 44, + year: '1996' + }, + { + medalType: 'Gold Medals', + count: 37, + year: '2000' + }, + { + medalType: 'Gold Medals', + count: 35, + year: '2004' + }, + { + medalType: 'Gold Medals', + count: 36, + year: '2008' + }, + { + medalType: 'Gold Medals', + count: 46, + year: '2012' + }, + { + medalType: 'Silver Medals', + count: 19, + year: '1952' + }, + { + medalType: 'Silver Medals', + count: 25, + year: '1956' + }, + { + medalType: 'Silver Medals', + count: 21, + year: '1960' + }, + { + medalType: 'Silver Medals', + count: 26, + year: '1964' + }, + { + medalType: 'Silver Medals', + count: 28, + year: '1968' + }, + { + medalType: 'Silver Medals', + count: 31, + year: '1972' + }, + { + medalType: 'Silver Medals', + count: 35, + year: '1976' + }, + { + medalType: 'Silver Medals', + count: null, + year: '1980' + }, + { + medalType: 'Silver Medals', + count: 60, + year: '1984' + }, + { + medalType: 'Silver Medals', + count: 31, + year: '1988' + }, + { + medalType: 'Silver Medals', + count: 34, + year: '1992' + }, + { + medalType: 'Silver Medals', + count: 32, + year: '1996' + }, + { + medalType: 'Silver Medals', + count: 24, + year: '2000' + }, + { + medalType: 'Silver Medals', + count: 40, + year: '2004' + }, + { + medalType: 'Silver Medals', + count: 38, + year: '2008' + }, + { + medalType: 'Silver Medals', + count: 29, + year: '2012' + }, + { + medalType: 'Bronze Medals', + count: 17, + year: '1952' + }, + { + medalType: 'Bronze Medals', + count: 17, + year: '1956' + }, + { + medalType: 'Bronze Medals', + count: 16, + year: '1960' + }, + { + medalType: 'Bronze Medals', + count: 28, + year: '1964' + }, + { + medalType: 'Bronze Medals', + count: 34, + year: '1968' + }, + { + medalType: 'Bronze Medals', + count: 30, + year: '1972' + }, + { + medalType: 'Bronze Medals', + count: 25, + year: '1976' + }, + { + medalType: 'Bronze Medals', + count: null, + year: '1980' + }, + { + medalType: 'Bronze Medals', + count: 30, + year: '1984' + }, + { + medalType: 'Bronze Medals', + count: 27, + year: '1988' + }, + { + medalType: 'Bronze Medals', + count: 37, + year: '1992' + }, + { + medalType: 'Bronze Medals', + count: 25, + year: '1996' + }, + { + medalType: 'Bronze Medals', + count: 33, + year: '2000' + }, + { + medalType: 'Bronze Medals', + count: 26, + year: '2004' + }, + { + medalType: 'Bronze Medals', + count: 36, + year: '2008' + }, + { + medalType: 'Bronze Medals', + count: 29, + year: '2012' + } + ] + }, + xField: 'year', + yField: 'count', + seriesField: 'medalType', + invalidType: 'link' + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-monotone.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-monotone.mjs new file mode 100644 index 0000000000..f38b0c8291 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-monotone.mjs @@ -0,0 +1,77 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0b52a1e9eec95f9ed4 + * 验证目的:单调平滑折线与点布局。 + * 保留条件:xField, yField, line;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: { + values: [ + { + time: '2:00', + value: 38 + }, + { + time: '4:00', + value: 56 + }, + { + time: '6:00', + value: 10 + }, + { + time: '8:00', + value: 70 + }, + { + time: '10:00', + value: 36 + }, + { + time: '12:00', + value: 94 + }, + { + time: '14:00', + value: 24 + }, + { + time: '16:00', + value: 44 + }, + { + time: '18:00', + value: 36 + }, + { + time: '20:00', + value: 68 + }, + { + time: '22:00', + value: 22 + } + ] + }, + xField: 'time', + yField: 'value', + line: { + style: { + curveType: 'monotone' + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-percent-negative.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-percent-negative.mjs new file mode 100644 index 0000000000..731af02d98 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-percent-negative.mjs @@ -0,0 +1,222 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0440 + * 验证目的:正负数据百分比堆叠折线。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'line', + padding: { + top: 50 + }, + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '164' + }, + { + x: '1', + type: 'A', + y: '10' + }, + { + x: '2', + type: 'A', + y: '-90' + }, + { + x: '3', + type: 'A', + y: '-112' + }, + { + x: '4', + type: 'A', + y: '-67' + }, + { + x: '5', + type: 'A', + y: '68' + }, + { + x: '6', + type: 'A', + y: '76' + }, + { + x: '7', + type: 'A', + y: '-91' + }, + { + x: '8', + type: 'A', + y: '74' + }, + { + x: '9', + type: 'A', + y: '65' + }, + { + x: '0', + type: 'B', + y: '136' + }, + { + x: '1', + type: 'B', + y: '10' + }, + { + x: '2', + type: 'B', + y: '57' + }, + { + x: '3', + type: 'B', + y: '16' + }, + { + x: '4', + type: 'B', + y: '-116' + }, + { + x: '5', + type: 'B', + y: '-46' + }, + { + x: '6', + type: 'B', + y: '-133' + }, + { + x: '7', + type: 'B', + y: '-10' + }, + { + x: '8', + type: 'B', + y: '-98' + }, + { + x: '9', + type: 'B', + y: '-40' + }, + { + x: '0', + type: 'C', + y: '136' + }, + { + x: '1', + type: 'C', + y: '10' + }, + { + x: '2', + type: 'C', + y: '57' + }, + { + x: '3', + type: 'C', + y: '16' + }, + { + x: '4', + type: 'C', + y: '-116' + }, + { + x: '5', + type: 'C', + y: '-46' + }, + { + x: '6', + type: 'C', + y: '-133' + }, + { + x: '7', + type: 'C', + y: '-10' + }, + { + x: '8', + type: 'C', + y: '-98' + }, + { + x: '9', + type: 'C', + y: '-40' + } + ] + } + ], + size: { + filed: 'y', + type: 'ordinal', + domain: [], + range: [1, 2] + }, + percent: true, + xField: 'x', + yField: 'y', + label: { + visible: true + }, + legends: [ + { + visible: true, + position: 'middle', + orient: 'left' + } + ], + seriesField: 'type', + point: { + state: { + hover: { + size: 20, + fill: 'red' + } + } + }, + axes: [ + { + orient: 'left', + tickCount: 6, + forceTickCount: 6, + visible: true + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ] + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-stack-dashed-legend.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-stack-dashed-legend.mjs new file mode 100644 index 0000000000..2ebcfaa7aa --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-stack-dashed-legend.mjs @@ -0,0 +1,232 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 652e02ee1dd15a4ab522f758 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:堆叠折线与底部虚线图例共存。 + * 保留条件:36 条原始数据、四组系列、lineH 图例虚线、标题。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'line', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked line chart' + }, + stack: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom', + item: { + shape: { + style: { + size: 20, + symbolType: 'lineH', + lineDash: [4, 4], + lineWidth: 2 + } + } + } + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/line/line-step.mjs b/packages/vchart/__tests__/visual/cases/charts/line/line-step.mjs new file mode 100644 index 0000000000..6a592fbd25 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/line/line-step.mjs @@ -0,0 +1,80 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0c52a1e9eec95f9ed6 + * 验证目的:阶梯折线的曲线类型与点布局。 + * 保留条件:xField, yField, point, line;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: { + values: [ + { + time: '2:00', + value: 38 + }, + { + time: '4:00', + value: 56 + }, + { + time: '6:00', + value: 10 + }, + { + time: '8:00', + value: 70 + }, + { + time: '10:00', + value: 36 + }, + { + time: '12:00', + value: 94 + }, + { + time: '14:00', + value: 24 + }, + { + time: '16:00', + value: 44 + }, + { + time: '18:00', + value: 36 + }, + { + time: '20:00', + value: 68 + }, + { + time: '22:00', + value: 22 + } + ] + }, + xField: 'time', + yField: 'value', + point: { + visible: false + }, + line: { + style: { + curveType: 'stepAfter' + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-body.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-body.mjs new file mode 100644 index 0000000000..7db6e68bc4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-body.mjs @@ -0,0 +1,46 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0342 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:四项原始数据映射饼图扇区。 + * 保留条件:原始数值和顺序、数值类目、data 对象。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'pie', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + categoryField: 'x', + valueField: 'y' + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-empty-placeholder.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-empty-placeholder.mjs new file mode 100644 index 0000000000..3fd0707890 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-empty-placeholder.mjs @@ -0,0 +1,42 @@ +import { verifySourceSpec, verifyEmptyPie } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66a8d642ef352c00ab73f490 + * 验证目的:空数据时显示自定义饼图占位环。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const spec = { + type: 'pie', + data: [ + { + values: [] + } + ], + radius: 0.8, + innerRadius: 0.5, + valueField: 'value', + categoryField: 'type', + emptyPlaceholder: { + showEmptyCircle: true, + emptyCircle: { + style: { + fill: '#ff00ffee', + innerRadius: 0.1 + } + } + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyEmptyPie(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-label-hover.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-label-hover.mjs new file mode 100644 index 0000000000..42569aba4a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-label-hover.mjs @@ -0,0 +1,103 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03e2 + * 验证目的:半圆饼图悬停状态及标签引导线布局。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'a', value: 100 }, + { type: 'b', value: 89 }, + { type: 'c', value: 123 }, + { type: 'd', value: 300 } + ] + } + ], + valueField: 'value', + categoryField: 'type', + radius: 0.8, + cornerRadius: 0.2, + center: { x: 150, y: 200 }, + startAngle: 90, + endAngle: 270, + padAngle: 10, + label: { + visible: true, + style: { fontSize: 16, text: args => '123123123' }, + state: { hover: { fill: 'red' } }, + line: { visible: true, style: { lineWidth: 2 }, state: { hover: { stroke: 'red' } } }, + layout: {} + }, + pie: { + style: { + stroke: '#aaa', + lineWidth: 2, + fill: { + gradient: 'radial', + r0: 0, + x0: 0.5, + y0: 0.5, + x1: 0.5, + y1: 0.5, + r1: 0.5, + stops: [ + { offset: 0, opacity: 0.2 }, + { offset: 0.5, opacity: 0.6 }, + { offset: 1, opacity: 1 } + ] + } + }, + state: { + hover: { + outerRadius: 0.85, + fill: { + gradient: 'radial', + r0: 0, + x0: 0.5, + y0: 0.5, + x1: 0.5, + y1: 0.5, + r1: 0.5, + stops: [ + { offset: 0, color: 'red' }, + { offset: 0.5, color: 'yellow' }, + { offset: 1, color: 'blue' } + ] + } + }, + selected: { stroke: 'blue', lineWidth: 2 } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'pie', 1); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(r => (window.__pieHover = r), await seriesStates(page)); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (!window.__pieHover?.some(m => m.name === 'pie' && m.states.hover > 0)) throw Error('半圆扇区未悬停'); + const texts = window.__visualChart + .getStage() + .findAll(g => g.type === 'text' && g.attribute.visible !== false, true); + if (!texts.length) throw Error('半圆标签未绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-negative.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-negative.mjs new file mode 100644 index 0000000000..be1f68d3df --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-negative.mjs @@ -0,0 +1,54 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66a9e4f5ef352c00ab73f4a7 + * 迁移方式:原配置迁移,类目文本匿名化。 + * 验证目的:混合正负值饼图同时开启全零显示和负值支持。 + * 保留条件:数据顺序及数值 1/2/-3、stack、outerRadius=0.8、label、showAllZero、supportNegative。 + * 覆盖边界:仅将类目文本匿名化,不去掉原始开关组合;不宣称复现未提供的历史缺陷。 + */ +export default { + createSpec() { + // 返回原始配置结构的新副本,只移除宿主生命周期依赖。 + return { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { + type: 'Alpha', + value: 1 + }, + { + type: 'Beta', + value: 2 + }, + { + type: 'Gamma', + value: -3 + } + ] + } + ], + label: { + visible: true + }, + stack: true, + outerRadius: 0.8, + valueField: 'value', + categoryField: 'type', + showAllZero: true, + supportNegative: true + }; + }, + async verify(page) { + // 精确核对所有原始数据、关联和开关,并检查真实系列状态。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const g = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + if (g.length !== 3 || g.some(g => !Number.isFinite(g.attribute.endAngle))) throw new Error('正负饼图扇区缺失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-nested.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-nested.mjs new file mode 100644 index 0000000000..6ef8b66a7d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-nested.mjs @@ -0,0 +1,131 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f1152a1e9eec95f9ee7 + * 验证目的:多系列嵌套环形饼图。 + * 保留条件:series, color, title, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + data: [ + { + id: 'id0', + values: [ + { + type: '0~29', + value: '126.04' + }, + { + type: '30~59', + value: '128.77' + }, + { + type: '60 and over', + value: '77.09' + } + ] + }, + { + id: 'id1', + values: [ + { + type: '0~9', + value: '39.12' + }, + { + type: '10~19', + value: '43.01' + }, + { + type: '20~29', + value: '43.91' + }, + { + type: '30~39', + value: '45.4' + }, + { + type: '40~49', + value: '40.89' + }, + { + type: '50~59', + value: '42.48' + }, + { + type: '60~69', + value: '39.63' + }, + { + type: '70~79', + value: '25.17' + }, + { + type: '80 and over', + value: '12.29' + } + ] + } + ], + series: [ + { + type: 'pie', + dataIndex: 0, + outerRadius: 0.65, + innerRadius: 0, + valueField: 'value', + categoryField: 'type', + label: { + position: 'inside', + visible: true, + style: { + fill: 'white' + } + }, + pie: { + style: { + stroke: '#ffffff', + lineWidth: 2 + } + } + }, + { + type: 'pie', + dataIndex: 1, + outerRadius: 0.8, + innerRadius: 0.67, + valueField: 'value', + categoryField: 'type', + label: { + visible: true + }, + pie: { + style: { + stroke: '#ffffff', + lineWidth: 2 + } + } + } + ], + color: ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56', '#d0743c', '#ff8c00'], + title: { + visible: true, + text: 'US population by age in 2021 (millions)' + }, + legends: { + visible: true, + orient: 'left' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-radius-scale.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-radius-scale.mjs new file mode 100644 index 0000000000..2f54e57204 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-radius-scale.mjs @@ -0,0 +1,103 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f1252a1e9eec95f9eea + * 验证目的:饼图半径字段编码。 + * 保留条件:width, valueField, categoryField, outerRadius, innerRadius, scales, label, color, title, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'pie', + width: 600, + data: [ + { + id: 'id0', + values: [ + { + type: '0~9', + value: '39.12' + }, + { + type: '10~19', + value: '43.01' + }, + { + type: '20~29', + value: '43.91' + }, + { + type: '30~39', + value: '45.4' + }, + { + type: '40~49', + value: '40.89' + }, + { + type: '50~59', + value: '42.48' + }, + { + type: '60~69', + value: '39.63' + }, + { + type: '70~79', + value: '25.17' + }, + { + type: '80 and over', + value: '12.29' + } + ] + } + ], + valueField: 'value', + categoryField: 'type', + outerRadius: { + field: 'value', + scale: 'outer-radius' + }, + innerRadius: { + field: 'value', + scale: 'inner-radius' + }, + scales: [ + { + id: 'outer-radius', + type: 'linear', + domain: [10, 50], + range: [120, 220] + }, + { + id: 'inner-radius', + type: 'linear', + domain: [10, 50], + range: [110, 10] + } + ], + label: { + visible: true, + position: 'inside' + }, + color: ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56', '#d0743c', '#ff8c00'], + title: { + visible: true, + text: 'US population by age in 2021 (millions)' + }, + legends: { + visible: true, + orient: 'right' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-show-all-zero.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-show-all-zero.mjs new file mode 100644 index 0000000000..08e4a1c00e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-show-all-zero.mjs @@ -0,0 +1,107 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66a8d730ef352c00ab73f491 + * 验证目的:showAllZero 下全零数据的扇区及外侧标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'pie', + data: [ + { + values: [ + { + value: 0, + type: 'Tradition Industries', + percentage: '71.6%' + }, + { + value: 0, + type: 'Business Companies', + percentage: '22.5%' + }, + { + value: 0, + type: 'Customer-facing Companies', + percentage: '5.9%' + } + ] + } + ], + radius: 0.8, + innerRadius: 0.5, + valueField: 'value', + categoryField: 'type', + label: { + visible: true, + style: { + fontSize: 16 + }, + line: { + style: {}, + line1MinLength: 30 + }, + layout: { + align: 'edge' + } + }, + pie: { + state: { + selected: { + outerRadius: 0.85 + } + } + }, + indicator: { + visible: true, + fixed: false, + trigger: 'select', + gap: 10, + title: { + field: 'type', + autoLimit: true, + style: { + fontSize: 16 + } + }, + content: [ + { + field: 'value', + style: { + fontSize: 42, + fontWeight: 'bolder' + } + }, + { + field: 'percentage', + style: { + fontSize: 20 + } + } + ] + }, + showAllZero: true + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'label'); + await page.evaluate(() => { + const graphics = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + if ( + graphics.length !== 3 || + graphics.some( + g => g.attribute.innerRadius <= 0 || Math.abs(g.attribute.endAngle - g.attribute.startAngle) < 0.01 + ) + ) + throw new Error('全零数据未呈现三个环形扇区'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/pie/pie-zero-placeholder.mjs b/packages/vchart/__tests__/visual/cases/charts/pie/pie-zero-placeholder.mjs new file mode 100644 index 0000000000..0f7a701215 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/pie/pie-zero-placeholder.mjs @@ -0,0 +1,63 @@ +import { verifySourceSpec, verifyEmptyPie } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 670cd7da124bb700a650178e + * 验证目的:全零数据时显示饼图占位环。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'oxygen', value: 0 }, + { type: 'silicon', value: 0 }, + { type: 'aluminum', value: 0 } + ] + } + ], + // supportNegative: true, + outerRadius: 0.8, + innerRadius: 0.5, + padAngle: 0.6, + valueField: 'value', + categoryField: 'type', + pie: { + style: { + cornerRadius: 10 + }, + state: { + hover: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + }, + selected: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + } + } + }, + legends: {}, + label: { + visible: true + }, + emptyPlaceholder: { + showEmptyCircle: true + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyEmptyPie(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-circular-gradient.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-circular-gradient.mjs new file mode 100644 index 0000000000..75b9aba35e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-circular-gradient.mjs @@ -0,0 +1,61 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03db + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:圆形进度渐变及内外留白。 + * 保留条件:原值 1、conical 渐变、roundCap、半径与 inner/outerPadding。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'circularProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 1 + } + ] + } + ], + valueField: 'value', + seriesField: 'type', + radiusField: 'type', + radius: 0.8, + innerRadius: 0.6, + roundCap: true, + cornerRadius: 5, + progress: { + style: { + innerPadding: 10, + outerPadding: 10, + cornerRadius: 20, + fill: { + gradient: 'conical', + stops: [ + { + offset: 0, + color: '#4FC6B4' + }, + { + offset: 1, + color: '#31679E' + } + ] + } + } + } + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-clamp.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-clamp.mjs new file mode 100644 index 0000000000..ac0f1196ee --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-clamp.mjs @@ -0,0 +1,59 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6800fda1281bec00b05008bd + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:超出数值轴上限的进度截断。 + * 保留条件:原值 5、数值轴 2..4、clamp=true、圆角及双轴。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'linearProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 5 + } + ] + } + ], + direction: 'horizontal', + xField: 'value', + yField: 'type', + clamp: true, + cornerRadius: 20, + bandWidth: 30, + axes: [ + { + orient: 'left', + label: { + visible: true + }, + type: 'band' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'linear', + min: 2, + max: 4 + } + ], + animation: false + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-gradient.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-gradient.mjs new file mode 100644 index 0000000000..c1b7271db3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-gradient.mjs @@ -0,0 +1,77 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03f6 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:渐变线性进度图与显式双轴。 + * 保留条件:0.6、线性渐变、圆角、bandWidth=30、轴标签。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'linearProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 0.6 + } + ] + } + ], + direction: 'horizontal', + xField: 'value', + yField: 'type', + cornerRadius: 20, + bandWidth: 30, + axes: [ + { + orient: 'left', + label: { + visible: true + }, + type: 'band' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'linear' + } + ], + progress: { + style: { + fill: { + gradient: 'linear', + x0: 0.4, + y0: 0.5, + x1: 1, + y1: 0.5, + stops: [ + { + offset: 0, + color: '#4FC6B4' + }, + { + offset: 1, + color: '#31679E' + } + ] + } + } + }, + animation: false + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-padding.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-padding.mjs new file mode 100644 index 0000000000..5551a77431 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-linear-padding.mjs @@ -0,0 +1,43 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03f9 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:进度主体上下留白。 + * 保留条件:0.7、bandWidth=50、topPadding/bottomPadding=10。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'linearProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 0.7 + } + ] + } + ], + direction: 'horizontal', + xField: 'value', + yField: 'type', + bandWidth: 50, + progress: { + topPadding: 10, + bottomPadding: 10 + }, + animation: false + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-series-track.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-series-track.mjs new file mode 100644 index 0000000000..a4ca6674a4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-series-track.mjs @@ -0,0 +1,50 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66dec3a612d09a05ebba9967 + * 验证目的:环形进度系列级轨道配置。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'circularProgress', + valueField: 'value', + categoryField: 'type', + data: [ + { + id: 'id0', + values: [ + { + type: 'Tradition Industries', + value: 0.795, + text: '79.5%' + } + ] + } + ], + series: [ + { + valueField: 'value', + categoryField: 'type', + track: { + style: { + fill: 'red' + } + }, + circularProgress: { + id: 'CircularProgress-0-circularProgress' + }, + type: 'circularProgress' + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-threshold-color.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-threshold-color.mjs new file mode 100644 index 0000000000..141df26ec1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-threshold-color.mjs @@ -0,0 +1,47 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03f8 + * 验证目的:线性进度阈值颜色映射。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'linearProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 0.7 + } + ] + } + ], + direction: 'horizontal', + xField: 'value', + yField: 'type', + cornerRadius: 20, + bandWidth: 30, + progress: { + style: { + fill: { + type: 'threshold', + field: 'value', + domain: [0.6, 0.8], + range: ['#D04D5B', '#ED9747', '#579E78'] + } + } + }, + animation: false + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-tick-mask.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-tick-mask.mjs new file mode 100644 index 0000000000..6db774ec87 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-tick-mask.mjs @@ -0,0 +1,166 @@ +import { verifySpec, verifyRendered, seriesGraphicCenter } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65e1ca90a5483e00afa58635 + * 验证目的:环形进度刻度遮罩、强制对齐及悬停指标。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:保留来源 move 动作,以实际进度图元定位替代旧宿主坐标;不改变数据或配置。 + * 覆盖边界:验证遮罩布局及 progress hover 留白、indicator 内容;不计选择或动画过程。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'circularProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'Tradition Industries', + value: 0.795, + text: '79.5%' + }, + { + type: 'Business Companies', + value: 0.5, + text: '50%' + }, + { + type: 'Customer-facing Companies', + value: 0.25, + text: '25%' + } + ] + } + ], + color: ['rgb(255, 222, 0)', 'rgb(171, 205, 5)', 'rgb(0, 154, 68)'], + valueField: 'value', + categoryField: 'type', + seriesField: 'type', + radius: 0.8, + innerRadius: 0.4, + progress: { + style: { + innerPadding: 5, + outerPadding: 5 + }, + state: { + hover: { + innerPadding: 0, + outerPadding: 0 + } + } + }, + tickMask: { + visible: true, + angle: 10, + offsetAngle: 0, + forceAlign: true, + style: { + cornerRadius: 15 + } + }, + axes: [ + { + visible: false, + type: 'linear', + orient: 'angle' + }, + { + visible: false, + type: 'band', + orient: 'radius' + } + ], + indicator: { + visible: true, + trigger: 'hover', + title: { + visible: true, + field: 'type', + autoLimit: true, + style: { + fontSize: 20, + fill: 'black' + } + }, + content: [ + { + visible: true, + field: 'text', + style: { + fontSize: 16, + fill: 'gray' + } + } + ] + }, + legends: { + visible: true, + orient: 'bottom', + title: { + visible: false + } + }, + animation: false + }; + }, + async exercise(page) { + // 源记录为一次 move;先检查初始留白,再悬停第一个有效进度图元。 + await page.evaluate(() => { + const chart = window.__visualChart; + const graphics = chart + .getChart() + .getAllSeries()[0] + .getMarks() + .find(mark => mark.name === 'progress') + .getGraphics(); + if ( + graphics.length !== 3 || + graphics.some(g => g.attribute.innerPadding !== -5 || g.attribute.outerPadding !== -5) + ) + throw new Error('进度初始留白不正确'); + if (chart.getStage().findAll(g => g.name === 'indicator-title' && g.attribute.visible !== false, true).length) + throw new Error('悬停前指标不应显示'); + }); + const point = await seriesGraphicCenter(page, 'progress'); + await page.mouse.move(point.x, point.y); + await page.waitForFunction(() => + window.__visualChart + .getChart() + .getAllSeries()[0] + .getMarks() + .find(mark => mark.name === 'progress') + .getGraphics() + .some(g => g.currentStates?.includes('hover')) + ); + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + // 同时验证图元状态与实际指标文字,跳过 move 时不能靠配置相同通过。 + await page.evaluate(() => { + const chart = window.__visualChart; + const graphics = chart + .getChart() + .getAllSeries()[0] + .getMarks() + .find(mark => mark.name === 'progress') + .getGraphics(); + const hovered = graphics.filter(g => g.currentStates?.includes('hover')); + if ( + hovered.length !== 1 || + hovered[0] !== graphics[0] || + hovered[0].attribute.innerPadding !== 0 || + hovered[0].attribute.outerPadding !== 0 + ) + throw new Error('进度 hover 留白未生效'); + const stage = chart.getStage(); + const title = stage.find(g => g.name === 'indicator-title' && g.attribute.visible !== false, true); + const content = stage.find(g => g.name === 'indicator-content-0' && g.attribute.visible !== false, true); + if (title?.attribute.text !== 'Tradition Industries' || content?.attribute.text !== '79.5%') + throw new Error('进度 hover 指标内容不正确'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/progress/progress-vertical-padding.mjs b/packages/vchart/__tests__/visual/cases/charts/progress/progress-vertical-padding.mjs new file mode 100644 index 0000000000..e42bcbe666 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/progress/progress-vertical-padding.mjs @@ -0,0 +1,76 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03f7 + * 验证目的:纵向线性进度左右内边距。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:仅检查静态最终状态;不计动画或未执行的交互配置。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'linearProgress', + data: [ + { + id: 'id0', + values: [ + { + type: 'A', + value: 0.2 + } + ] + } + ], + direction: 'vertical', + xField: 'type', + yField: 'value', + cornerRadius: 20, + bandWidth: 30, + axes: [ + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + }, + { + orient: 'left', + label: { + visible: true + }, + type: 'linear' + } + ], + progress: { + leftPadding: 0, + rightPadding: 0, + style: { + fill: { + gradient: 'linear', + x0: 0.5, + y0: 0.2, + x1: 0.5, + y1: 0, + stops: [ + { + offset: 0, + color: '#4FC6B4' + }, + { + offset: 1, + color: '#31679E' + } + ] + } + } + }, + animation: false + }; + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/radar/radar-negative.mjs b/packages/vchart/__tests__/visual/cases/charts/radar/radar-negative.mjs new file mode 100644 index 0000000000..ba6c8b871a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/radar/radar-negative.mjs @@ -0,0 +1,117 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64ce5d63a77f904af107fe72 + * 验证目的:雷达图负值与面积区域。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'radar', + data: [ + { + id: 'Platinum', + values: [ + { + key: 'Power', + value: 5 + }, + { + key: 'Speed', + value: -15 + }, + { + key: 'Range', + value: 3 + }, + { + key: 'Stamina', + value: 5 + }, + { + key: 'Precision', + value: 5 + }, + { + key: 'Growth', + value: 5 + } + ] + } + ], + categoryField: 'key', + valueField: 'value', + point: { + visible: false // 不展示点 + }, + area: { + visible: true, // 展示面积 + state: { + // 面积 hover 状态下的样式 + hover: { + fillOpacity: 0.5 + } + } + }, + line: { + style: { + lineWidth: 4 + } + }, + axes: [ + { + orient: 'radius', // 半径轴配置 + zIndex: 100, + + max: 8, + domainLine: { + visible: false + }, + label: { + visible: true, + space: 0, + style: { + textAlign: 'center', + stroke: '#fff', + lineWidth: 4 + } + }, + grid: { + smooth: false, + style: { + lineDash: [0] + } + } + }, + { + orient: 'angle', // 角度轴配置 + zIndex: 50, + tick: { + visible: false + }, + domainLine: { + visible: false + }, + label: { + space: 20 + }, + grid: { + style: { + lineDash: [0] + } + } + } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/radar/radar-series.mjs b/packages/vchart/__tests__/visual/cases/charts/radar/radar-series.mjs new file mode 100644 index 0000000000..d191307484 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/radar/radar-series.mjs @@ -0,0 +1,77 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e031f + * 验证目的:雷达图多系列与角度和半径轴。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const spec = { + type: 'radar', + data: [ + { + id: 'radar', + values: [ + { theta: '0', type: 'A', r: '827' }, + { theta: '1', type: 'A', r: '608' }, + { theta: '2', type: 'A', r: '643' }, + { theta: '3', type: 'A', r: '899' }, + { theta: '4', type: 'A', r: '612' }, + { theta: '5', type: 'A', r: '715' }, + { theta: '6', type: 'A', r: '800' }, + { theta: '7', type: 'A', r: '674' }, + { theta: '8', type: 'A', r: '601' }, + { theta: '9', type: 'A', r: '620' }, + { theta: '0', type: 'B', r: '776' }, + { theta: '1', type: 'B', r: '816' }, + { theta: '2', type: 'B', r: '881' }, + { theta: '3', type: 'B', r: '767' }, + { theta: '4', type: 'B', r: '748' }, + { theta: '5', type: 'B', r: '682' }, + { theta: '6', type: 'B', r: '811' }, + { theta: '7', type: 'B', r: '712' }, + { theta: '8', type: 'B', r: '783' }, + { theta: '9', type: 'B', r: '624' } + ] + } + ], + categoryField: 'theta', + valueField: 'r', + seriesField: 'type', + line: { + style: { + lineWidth: 8 + } + }, + // center: { x: 200, y: 300 }, + startAngle: 90, + axes: [ + { + orient: 'angle', + domainLine: { visible: true, smooth: false }, + tick: { visible: true }, + grid: { visible: true }, + label: { visible: true } + }, + { + orient: 'radius', + label: { visible: true }, + domainLine: { visible: true, smooth: false }, + tick: { visible: true }, + grid: { visible: true, smooth: false, style: { stroke: 'red', zIndex: 110 } } + } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/radar/radar-stacked.mjs b/packages/vchart/__tests__/visual/cases/charts/radar/radar-stacked.mjs new file mode 100644 index 0000000000..3bcfc4c848 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/radar/radar-stacked.mjs @@ -0,0 +1,246 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f1552a1e9eec95f9ef4 + * 验证目的:雷达面积堆叠与圆形径向网格。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'radar', + data: [ + { + values: [ + { + month: 'Month 1', + value: 45, + type: 'A' + }, + { + month: 'Month 2', + value: 61, + type: 'A' + }, + { + month: 'Month 3', + value: 92, + type: 'A' + }, + { + month: 'Month 4', + value: 57, + type: 'A' + }, + { + month: 'Month 5', + value: 46, + type: 'A' + }, + { + month: 'Month 6', + value: 36, + type: 'A' + }, + { + month: 'Month 7', + value: 33, + type: 'A' + }, + { + month: 'Month 8', + value: 63, + type: 'A' + }, + { + month: 'Month 9', + value: 57, + type: 'A' + }, + { + month: 'Month 10', + value: 53, + type: 'A' + }, + { + month: 'Month 11', + value: 69, + type: 'A' + }, + { + month: 'Month 12', + value: 40, + type: 'A' + }, + { + month: 'Month 1', + value: 31, + type: 'B' + }, + { + month: 'Month 2', + value: 39, + type: 'B' + }, + { + month: 'Month 3', + value: 81, + type: 'B' + }, + { + month: 'Month 4', + value: 39, + type: 'B' + }, + { + month: 'Month 5', + value: 64, + type: 'B' + }, + { + month: 'Month 6', + value: 21, + type: 'B' + }, + { + month: 'Month 7', + value: 58, + type: 'B' + }, + { + month: 'Month 8', + value: 72, + type: 'B' + }, + { + month: 'Month 9', + value: 47, + type: 'B' + }, + { + month: 'Month 10', + value: 37, + type: 'B' + }, + { + month: 'Month 11', + value: 80, + type: 'B' + }, + { + month: 'Month 12', + value: 74, + type: 'B' + }, + { + month: 'Month 1', + value: 90, + type: 'C' + }, + { + month: 'Month 2', + value: 95, + type: 'C' + }, + { + month: 'Month 3', + value: 62, + type: 'C' + }, + { + month: 'Month 4', + value: 52, + type: 'C' + }, + { + month: 'Month 5', + value: 74, + type: 'C' + }, + { + month: 'Month 6', + value: 87, + type: 'C' + }, + { + month: 'Month 7', + value: 80, + type: 'C' + }, + { + month: 'Month 8', + value: 69, + type: 'C' + }, + { + month: 'Month 9', + value: 74, + type: 'C' + }, + { + month: 'Month 10', + value: 84, + type: 'C' + }, + { + month: 'Month 11', + value: 94, + type: 'C' + }, + { + month: 'Month 12', + value: 23, + type: 'C' + } + ] + } + ], + categoryField: 'month', + valueField: 'value', + seriesField: 'type', // 声明分组字段 + stack: true, + area: { + visible: true // 展示面积 + }, + axes: [ + { + orient: 'radius', // 半径轴配置 + min: 0, + domainLine: { + visible: true + }, + label: { + visible: true + }, + grid: { + smooth: true // 平滑的网格线 + } + }, + { + orient: 'angle', // 角度轴配置 + tick: { + visible: false + }, + grid: { + style: { + lineDash: [0] + } + } + } + ], + legends: { + visible: true, + orient: 'top' + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-average-line.mjs b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-average-line.mjs new file mode 100644 index 0000000000..d2d480a1d3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-average-line.mjs @@ -0,0 +1,72 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7ee452a1e9eec95f9e6a, 646745e9cb5fa8011f4e0462 + * 迁移边界:合并两个来源相同的静态范围带与平均线;旧 strokeWidth 与 lineWidth 的 hover 差异未算作交互覆盖。 + * 验证目的:独立数据源的区间面积与平均值折线组合。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'common', + data: [ + { + id: 'areaData', + values: [ + { type: 'Category A', min: 76, max: 100 }, + { type: 'Category B', min: 56, max: 108 }, + { type: 'Category C', min: 38, max: 129 }, + { type: 'Category D', min: 58, max: 155 }, + { type: 'Category E', min: 45, max: 120 }, + { type: 'Category F', min: 23, max: 99 }, + { type: 'Category G', min: 18, max: 56 }, + { type: 'Category H', min: 18, max: 34 } + ] + }, + { + id: 'lineData', + values: [ + { type: 'Category A', average: 88 }, + { type: 'Category B', average: 82 }, + { type: 'Category C', average: 83.5 }, + { type: 'Category D', average: 106.5 }, + { type: 'Category E', average: 82.5 }, + { type: 'Category F', average: 61 }, + { type: 'Category G', average: 37 }, + { type: 'Category H', average: 26 } + ] + } + ], + series: [ + { + type: 'rangeArea', + dataIndex: 0, + xField: 'type', + yField: ['min', 'max'], + stack: false, + area: { style: { fillOpacity: 0.15 } } + }, + { + type: 'line', + dataIndex: 1, + xField: 'type', + yField: 'average', + point: { state: { hover: { fillOpacity: 0.5, stroke: 'blue', lineWidth: 2 }, selected: { fill: 'red' } } } + } + ], + axes: [ + { orient: 'left', label: { visible: true }, type: 'linear' }, + { orient: 'bottom', type: 'band' } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-horizontal.mjs b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-horizontal.mjs new file mode 100644 index 0000000000..7291dc17f9 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-horizontal.mjs @@ -0,0 +1,56 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0461 + * 验证目的:横向区间面积图的上下界。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const spec = { + type: 'rangeArea', + data: [ + { + id: 'areaData', + values: [ + { type: 'Category 1', min: 76, max: 100 }, + { type: 'Category 2', min: 56, max: 108 }, + { type: 'Category 3', min: 38, max: 129 }, + { type: 'Category 4', min: 58, max: 155 }, + { type: 'Category 5', min: 45, max: 120 }, + { type: 'Category 6', min: 23, max: 99 }, + { type: 'Category 7', min: 18, max: 56 }, + { type: 'Category 8', min: 18, max: 34 } + ] + } + ], + yField: 'type', + xField: ['min', 'max'], + minField: 'min', + maxField: 'max', + + direction: 'horizontal', + + axes: [ + { + orient: 'bottom', + label: { + visible: true + }, + type: 'linear' + }, + { orient: 'left', type: 'band' } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-missing-bound.mjs b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-missing-bound.mjs new file mode 100644 index 0000000000..067dc97cc5 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-missing-bound.mjs @@ -0,0 +1,111 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65f295d9603de700d1d8ba7e + * 验证目的:区间面积上界缺失并叠加两条折线。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + // Original issue link omitted. + const spec = { + type: 'common', + data: [ + { + id: 'areaData', + values: [ + { year: '1700', exports: 35, imports: 70 }, + { year: '1710', exports: 59, imports: 81 }, + { year: '1720', exports: 76, imports: 26 }, + { year: '1730', exports: 65, imports: 197 }, + { year: '1740', exports: 67, imports: 93 }, + { year: '1750', exports: 79, imports: 90 }, + { year: '1753', exports: 87 }, + { year: '1760', exports: 115 }, + { year: '1770', exports: 163 }, + { year: '1780', exports: 185 } + ] + } + ], + series: [ + { + type: 'rangeArea', + xField: 'year', + invalidType: 'break', + yField: ['exports', 'imports'], + area: { + style: { + curveType: 'monotone', + fillOpacity: 0.5, + fill: 'pink' + } + } + }, + { + type: 'line', + xField: 'year', + yField: 'exports', + point: { + style: { + size: 0 + } + }, + line: { + style: { + curveType: 'monotone', + stroke: '#5541FF', + lineDash: [5, 5] + } + } + }, + { + type: 'line', + xField: 'year', + yField: 'imports', + point: { + style: { + size: 0 + } + }, + line: { + style: { + curveType: 'monotone', + stroke: '#25C3EA' + } + } + } + ], + axes: [ + { + orient: 'left', + visible: false, + type: 'linear' + }, + { + orient: 'bottom', + grid: { + visible: true, + style: { + stroke: '#4C50D2', + lineWidth: 35, + opacity: 0.05 + } + }, + domainLine: false, + tick: false + } + ], + background: '#080550', + width: 800 + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-vertical.mjs b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-vertical.mjs new file mode 100644 index 0000000000..f140896280 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/range-area/range-area-vertical.mjs @@ -0,0 +1,46 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0460 + * 验证目的:纵向区间面积的上下界及半透明填充。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'rangeArea', + data: [ + { + id: 'areaData', + values: [ + { type: 'Category A', min: 76, max: 100 }, + { type: 'Category B', min: 56, max: 108 }, + { type: 'Category C', min: 38, max: 129 }, + { type: 'Category D', min: 58, max: 155 }, + { type: 'Category E', min: 45, max: 120 }, + { type: 'Category F', min: 23, max: 99 }, + { type: 'Category G', min: 18, max: 56 }, + { type: 'Category H', min: 18, max: 34 } + ] + } + ], + xField: 'type', + yField: ['min', 'max'], + minField: 'min', + maxField: 'max', + area: { style: { fillOpacity: 0.15 } }, + axes: [ + { orient: 'left', label: { visible: true }, type: 'linear' }, + { orient: 'bottom', type: 'band' } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/range-column/range-column-horizontal.mjs b/packages/vchart/__tests__/visual/cases/charts/range-column/range-column-horizontal.mjs new file mode 100644 index 0000000000..a3f05588f6 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/range-column/range-column-horizontal.mjs @@ -0,0 +1,90 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e040a + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:横向区间柱与端点标签。 + * 保留条件:八个原始区间、min/max、双轴及标签。 + * 改写说明:仅替换说明中提及的通用类目或标题文本;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'rangeColumn', + data: [ + { + id: 'data0', + values: [ + { + type: 'Category 1', + min: 76, + max: 100 + }, + { + type: 'Category 2', + min: 56, + max: 108 + }, + { + type: 'Category 3', + min: 38, + max: 129 + }, + { + type: 'Category 4', + min: 58, + max: 155 + }, + { + type: 'Category 5', + min: 45, + max: 120 + }, + { + type: 'Category 6', + min: 23, + max: 99 + }, + { + type: 'Category 7', + min: 18, + max: 56 + }, + { + type: 'Category 8', + min: 18, + max: 34 + } + ] + } + ], + direction: 'horizontal', + yField: 'type', + minField: 'min', + maxField: 'max', + axes: [ + { + orient: 'left', + type: 'band' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'linear' + } + ], + label: { + visible: true + } + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/rose/rose-group.mjs b/packages/vchart/__tests__/visual/cases/charts/rose/rose-group.mjs new file mode 100644 index 0000000000..917a8d684e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/rose/rose-group.mjs @@ -0,0 +1,364 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f1652a1e9eec95f9ef9 + * 验证目的:玫瑰图分组与极坐标轴。 + * 保留条件:categoryField, valueField, seriesField, outerRadius, title, stack, legends, color, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'rose', + data: [ + { + values: [ + { + time: '12814', + month: 'Jan', + level: '0-3', + location: 'Coastal' + }, + { + time: '3054', + month: 'Jan', + level: '3-6', + location: 'Coastal' + }, + { + time: '4376', + month: 'Jan', + level: '6-9', + location: 'Coastal' + }, + { + time: '4229', + month: 'Jan', + level: '9-12', + location: 'Coastal' + }, + { + time: '8814', + month: 'Feb', + level: '0-3', + location: 'Coastal' + }, + { + time: '5067', + month: 'Feb', + level: '3-6', + location: 'Coastal' + }, + { + time: '13987', + month: 'Feb', + level: '6-9', + location: 'Coastal' + }, + { + time: '3932', + month: 'Feb', + level: '9-12', + location: 'Coastal' + }, + { + time: '11624', + month: 'Mar', + level: '0-3', + location: 'Coastal' + }, + { + time: '7004', + month: 'Mar', + level: '3-6', + location: 'Coastal' + }, + { + time: '3574', + month: 'Mar', + level: '6-9', + location: 'Coastal' + }, + { + time: '5221', + month: 'Mar', + level: '9-12', + location: 'Coastal' + }, + { + time: '8814', + month: 'Apr', + level: '0-3', + location: 'Coastal' + }, + { + time: '9054', + month: 'Apr', + level: '3-6', + location: 'Coastal' + }, + { + time: '4376', + month: 'Apr', + level: '6-9', + location: 'Coastal' + }, + { + time: '5256', + month: 'Apr', + level: '9-12', + location: 'Coastal' + }, + { + time: '9998', + month: 'May', + level: '0-3', + location: 'Coastal' + }, + { + time: '5043', + month: 'May', + level: '3-6', + location: 'Coastal' + }, + { + time: '4572', + month: 'May', + level: '6-9', + location: 'Coastal' + }, + { + time: '3308', + month: 'May', + level: '9-12', + location: 'Coastal' + }, + { + time: '12321', + month: 'Jun', + level: '0-3', + location: 'Coastal' + }, + { + time: '15067', + month: 'Jun', + level: '3-6', + location: 'Coastal' + }, + { + time: '3417', + month: 'Jun', + level: '6-9', + location: 'Coastal' + }, + { + time: '5432', + month: 'Jun', + level: '9-12', + location: 'Coastal' + }, + { + time: '8912', + month: 'Jan', + level: '0-3', + location: 'Inland' + }, + { + time: '1753', + month: 'Jan', + level: '3-6', + location: 'Inland' + }, + { + time: '1905', + month: 'Jan', + level: '6-9', + location: 'Inland' + }, + { + time: '2057', + month: 'Jan', + level: '9-12', + location: 'Inland' + }, + { + time: '6987', + month: 'Feb', + level: '0-3', + location: 'Inland' + }, + { + time: '1873', + month: 'Feb', + level: '3-6', + location: 'Inland' + }, + { + time: '8017', + month: 'Feb', + level: '6-9', + location: 'Inland' + }, + { + time: '3056', + month: 'Feb', + level: '9-12', + location: 'Inland' + }, + { + time: '8124', + month: 'Mar', + level: '0-3', + location: 'Inland' + }, + { + time: '6900', + month: 'Mar', + level: '3-6', + location: 'Inland' + }, + { + time: '2768', + month: 'Mar', + level: '6-9', + location: 'Inland' + }, + { + time: '1070', + month: 'Mar', + level: '9-12', + location: 'Inland' + }, + { + time: '3986', + month: 'Apr', + level: '0-3', + location: 'Inland' + }, + { + time: '7986', + month: 'Apr', + level: '3-6', + location: 'Inland' + }, + { + time: '1453', + month: 'Apr', + level: '6-9', + location: 'Inland' + }, + { + time: '3215', + month: 'Apr', + level: '9-12', + location: 'Inland' + }, + { + time: '7905', + month: 'May', + level: '0-3', + location: 'Inland' + }, + { + time: '4908', + month: 'May', + level: '3-6', + location: 'Inland' + }, + { + time: '1030', + month: 'May', + level: '6-9', + location: 'Inland' + }, + { + time: '852', + month: 'May', + level: '9-12', + location: 'Inland' + }, + { + time: '3018', + month: 'Jun', + level: '0-3', + location: 'Inland' + }, + { + time: '8954', + month: 'Jun', + level: '3-6', + location: 'Inland' + }, + { + time: '1395', + month: 'Jun', + level: '6-9', + location: 'Inland' + }, + { + time: '3520', + month: 'Jun', + level: '9-12', + location: 'Inland' + } + ] + } + ], + categoryField: ['month', 'location'], + valueField: 'time', + seriesField: 'level', + outerRadius: 1, + title: { + visible: true, + text: 'Wind speed by region in first half of year' + }, + stack: true, + legends: [ + { + visible: true, + position: 'middle', + orient: 'left' + } + ], + color: ['#FF6D60', '#F7D060', '#F3E99F', '#98D8AA'], + axes: [ + { + orient: 'angle', + domainLine: { + visible: true, + smooth: true + }, + label: { + visible: true + }, + tick: { + visible: true + }, + grid: { + visible: true, + alignWithLabel: false + }, + paddingInner: 0.05, + paddingOuter: 0.1 + }, + { + orient: 'radius', + label: { + visible: true + }, + grid: { + visible: true, + smooth: true + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/rose/rose-stack.mjs b/packages/vchart/__tests__/visual/cases/charts/rose/rose-stack.mjs new file mode 100644 index 0000000000..6d0261f84e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/rose/rose-stack.mjs @@ -0,0 +1,194 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f1752a1e9eec95f9efa + * 验证目的:玫瑰图分组数据的堆叠。 + * 保留条件:categoryField, valueField, seriesField, outerRadius, stack, title, legends, color, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'rose', + data: [ + { + values: [ + { + time: '12814', + month: 'Jan', + level: '0-3' + }, + { + time: '3054', + month: 'Jan', + level: '3-6' + }, + { + time: '4376', + month: 'Jan', + level: '6-9' + }, + { + time: '4229', + month: 'Jan', + level: '9-12' + }, + { + time: '8814', + month: 'Feb', + level: '0-3' + }, + { + time: '5067', + month: 'Feb', + level: '3-6' + }, + { + time: '13987', + month: 'Feb', + level: '6-9' + }, + { + time: '3932', + month: 'Feb', + level: '9-12' + }, + { + time: '11624', + month: 'Mar', + level: '0-3' + }, + { + time: '7004', + month: 'Mar', + level: '3-6' + }, + { + time: '3574', + month: 'Mar', + level: '6-9' + }, + { + time: '5221', + month: 'Mar', + level: '9-12' + }, + { + time: '8814', + month: 'Apr', + level: '0-3' + }, + { + time: '9054', + month: 'Apr', + level: '3-6' + }, + { + time: '4376', + month: 'Apr', + level: '6-9' + }, + { + time: '5256', + month: 'Apr', + level: '9-12' + }, + { + time: '9998', + month: 'May', + level: '0-3' + }, + { + time: '5043', + month: 'May', + level: '3-6' + }, + { + time: '4572', + month: 'May', + level: '6-9' + }, + { + time: '3308', + month: 'May', + level: '9-12' + }, + { + time: '12321', + month: 'Jun', + level: '0-3' + }, + { + time: '15067', + month: 'Jun', + level: '3-6' + }, + { + time: '3417', + month: 'Jun', + level: '6-9' + }, + { + time: '5432', + month: 'Jun', + level: '9-12' + } + ] + } + ], + categoryField: 'month', + valueField: 'time', + seriesField: 'level', + outerRadius: 1, + stack: true, + title: { + visible: true, + text: 'Wind speed in first half of year' + }, + legends: [ + { + visible: true, + position: 'middle', + orient: 'left' + } + ], + color: ['#FFB84C', '#F266AB', '#A459D1', '#2CD3E1'], + axes: [ + { + orient: 'angle', + domainLine: { + visible: true, + smooth: true + }, + label: { + visible: true + }, + tick: { + visible: true + }, + grid: { + visible: true + }, + bandPadding: 0.05 + }, + { + orient: 'radius', + label: { + visible: true + }, + grid: { + visible: true, + smooth: true + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-adjacency.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-adjacency.mjs new file mode 100644 index 0000000000..db08a12357 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-adjacency.mjs @@ -0,0 +1,166 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 6527b98851d9f60a3c5cb802 + * 验证目的:点击桑基节点时仅高亮相邻节点与连接,空白点击恢复。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'sankey', + data: [ + { + values: [ + { + nodes: [ + { name: 'Berlin' }, + { name: 'Job Applications' }, + { name: 'Barcelona' }, + { name: 'Madrid' }, + { name: 'Amsterdam' }, + { name: 'Paris' }, + { name: 'London' }, + { name: 'Munich' }, + { name: 'Brussels' }, + { name: 'Dubai' }, + { name: 'Dublin' }, + { name: 'Other Cities' }, + { name: 'No Response' }, + { name: 'Responded' }, + { name: 'Rejected' }, + { name: 'Interviewed' }, + { name: 'No Offer' }, + { name: 'Declined Offer' }, + { name: 'Accepted Offer' } + ], + links: [ + { source: 'Berlin', target: 'Job Applications', value: 102, color: '#dddddd' }, + { source: 'Barcelona', target: 'Job Applications', value: 39, color: '#dddddd' }, + { source: 'Madrid', target: 'Job Applications', value: 35, color: '#dddddd' }, + { source: 'Amsterdam', target: 'Job Applications', value: 15, color: '#dddddd' }, + { source: 'Paris', target: 'Job Applications', value: 14, color: '#dddddd' }, + { source: 'London', target: 'Job Applications', value: 6, color: '#dddddd' }, + { source: 'Munich', target: 'Job Applications', value: 5, color: '#dddddd' }, + { source: 'Brussels', target: 'Job Applications', value: 4, color: '#dddddd' }, + { source: 'Dubai', target: 'Job Applications', value: 3, color: '#dddddd' }, + { source: 'Dublin', target: 'Job Applications', value: 3, color: '#dddddd' }, + { source: 'Other Cities', target: 'Job Applications', value: 12, color: '#dddddd' }, + { source: 'Job Applications', target: 'No Response', value: 189, color: '#dddddd' }, + { source: 'Job Applications', target: 'Responded', value: 49, color: 'orange' }, + { source: 'Responded', target: 'Rejected', value: 38, color: '#dddddd' }, + { source: 'Responded', target: 'Interviewed', value: 11, color: 'orange' }, + { source: 'Interviewed', target: 'No Offer', value: 8, color: '#dddddd' }, + { source: 'Interviewed', target: 'Declined Offer', value: 2, color: '#dddddd' }, + { source: 'Interviewed', target: 'Accepted Offer', value: 1, color: 'orange' } + ] + } + ] + } + ], + categoryField: 'name', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 15, + minNodeHeight: 4, + nodeKey: datum => datum.name, + iterations: 20, + title: { text: 'Job application process' }, + label: { + visible: true, + style: { fontSize: 10, fill: 'black' }, + state: { blur: { fill: '#e8e8e8', fillOpacity: 0.15 } } + }, + node: { + style: { fill: '#b9b9b9', stroke: 'white', lineWidth: 1, strokeOpacity: 1 }, + state: { + hover: { fill: 'red', fillOpacity: 1 }, + selected: { fill: '#dddddd', stroke: '#333333', lineWidth: 1, fillOpacity: 1 }, + blur: { fillOpacity: 0.05, strokeOpacity: 0.05 } + } + }, + tooltip: { transitionDuration: 0 }, + link: { + style: { + fill: data => { + return data.color ?? data.datum.color; + }, + fillOpacity: 1 + }, + state: { hover: { fillOpacity: 1 }, selected: { fillOpacity: 1 }, blur: { fillOpacity: 0.05 } } + }, + emphasis: { enable: true, effect: 'adjacency' } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const records = []; + for (const index of [0, -1, 1]) { + if (index < 0) await page.mouse.click(790, 590); + else { + const p = await interactionTarget(page, 'node', index); + await page.mouse.click(p.x, p.y); + } + await page.mouse.move(950, 750); + await interactionFrame(page); + records.push( + await page.evaluate(index => { + const s = window.__visualChart.getChart().getAllSeries()[0], + nodes = s + .getMarks() + .find(m => m.name === 'node') + .getGraphics(), + links = s + .getMarks() + .find(m => m.name === 'link') + .getGraphics(); + return { + index, + key: index < 0 ? null : nodes[index].context.data[0].key, + nodes: nodes.map(g => ({ key: g.context.data[0].key, states: g.currentStates })), + links: links.map(g => ({ + source: g.context.data[0].source, + target: g.context.data[0].target, + states: g.currentStates + })) + }; + }, index) + ); + } + await page.evaluate(r => (window.__adjacency = r), records); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const rows = window.__adjacency; + if (rows?.length !== 3) throw Error('缺少邻接状态过程'); + for (const r of rows) { + if (r.index < 0) { + if ([...r.nodes, ...r.links].some(g => g.states.some(s => ['selected', 'blur'].includes(s)))) + throw Error('邻接高亮未清除'); + continue; + } + const neighbors = new Set([r.key]); + for (const l of r.links) { + const match = l.source === r.key || l.target === r.key; + if (match) { + neighbors.add(l.source); + neighbors.add(l.target); + } + if (!l.states.includes(match ? 'selected' : 'blur')) throw Error('链接邻接状态错误'); + } + for (const n of r.nodes) + if (!n.states.includes(neighbors.has(n.key) ? 'selected' : 'blur')) throw Error('节点邻接状态错误'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-end.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-end.mjs new file mode 100644 index 0000000000..aa9f08e1f1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-end.mjs @@ -0,0 +1,567 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66dea2f612d09a05ebba9953 + * 验证目的:桑基节点交叉方向末端对齐。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'sankey', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'data', + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sun energy PV' + }, + { + nodeName: 'Sun energy Thermal' + }, + { + nodeName: 'Sun energy' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + crossNodeAlign: 'end', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + label: { + visible: true, + style: { + fontSize: 10 + }, + state: { + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + node: { + state: { + hover: { + fill: 'red' + }, + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + link: { + state: { + hover: { + fillOpacity: 1 + }, + blur: { + fill: '#e8e8e8' + } + } + }, + selected: { + enable: true, + effect: 'adjacency' + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-middle.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-middle.mjs new file mode 100644 index 0000000000..49d26efe12 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-middle.mjs @@ -0,0 +1,567 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66dea31f12d09a05ebba9954 + * 验证目的:桑基节点交叉方向中间对齐。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'sankey', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'data', + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sun energy PV' + }, + { + nodeName: 'Sun energy Thermal' + }, + { + nodeName: 'Sun energy' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + crossNodeAlign: 'middle', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + label: { + visible: true, + style: { + fontSize: 10 + }, + state: { + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + node: { + state: { + hover: { + fill: 'red' + }, + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + link: { + state: { + hover: { + fillOpacity: 1 + }, + blur: { + fill: '#e8e8e8' + } + } + }, + selected: { + enable: true, + effect: 'adjacency' + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-start.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-start.mjs new file mode 100644 index 0000000000..4efe7edbec --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-cross-align-start.mjs @@ -0,0 +1,567 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66dea2c812d09a05ebba9952 + * 验证目的:桑基节点交叉方向起点对齐。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'sankey', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'data', + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sun energy PV' + }, + { + nodeName: 'Sun energy Thermal' + }, + { + nodeName: 'Sun energy' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + crossNodeAlign: 'start', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + label: { + visible: true, + style: { + fontSize: 10 + }, + state: { + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + node: { + state: { + hover: { + fill: 'red' + }, + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + link: { + state: { + hover: { + fillOpacity: 1 + }, + blur: { + fill: '#e8e8e8' + } + } + }, + selected: { + enable: true, + effect: 'adjacency' + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-horizontal.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-horizontal.mjs new file mode 100644 index 0000000000..9ac2f16365 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-horizontal.mjs @@ -0,0 +1,567 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e045c + * 验证目的:横向桑基图节点和连接布局。 + * 保留条件:width, height, padding, categoryField, valueField, sourceField, targetField, nodeAlign, nodeGap, nodeWidth, minNodeHeight, label, node, link, selected;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'sankey', + width: 800, + height: 400, + padding: 5, + data: [ + { + name: 'data', + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sunlight PV' + }, + { + nodeName: 'Sunlight thermal' + }, + { + nodeName: 'Sunlight' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + label: { + visible: true, + style: { + fontSize: 10 + }, + state: { + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + node: { + state: { + hover: { + fill: 'red' + }, + blur: { + fill: '#e8e8e8', + fillOpacity: 0.15 + } + } + }, + link: { + state: { + hover: { + fillOpacity: 1 + }, + blur: { + fill: '#e8e8e8' + } + } + }, + selected: { + enable: true, + effect: 'adjacency' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-align.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-align.mjs new file mode 100644 index 0000000000..a2dea15ff4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-align.mjs @@ -0,0 +1,566 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f1752a1e9eec95f9efb + * 验证目的:桑基节点左对齐。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'sankey', + data: [ + { + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sun energy PV' + }, + { + nodeName: 'Sun energy Thermal' + }, + { + nodeName: 'Sun energy' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + nodeAlign: 'left', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + title: { + text: 'How energy is converted or transmitted before being consumed or lost', + subtext: 'Data: Department of Energy & Climate Change via Tom Counsell', + subtextStyle: { + fontSize: 12 + } + }, + label: { + visible: true, + style: { + fontSize: 10 + } + }, + node: { + state: { + hover: { + stroke: '#333333' + }, + selected: { + fill: '#dddddd', + stroke: '#333333', + lineWidth: 1, + brighter: 1, + fillOpacity: 1 + } + } + }, + link: { + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: '#dddddd', + stroke: '#333333', + lineWidth: 1, + brighter: 1, + fillOpacity: 1 + } + } + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-state.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-state.mjs new file mode 100644 index 0000000000..2e1c8ff4bc --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-node-state.mjs @@ -0,0 +1,197 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7f1752a1e9eec95f9efc + * 验证目的:来源桑基节点悬停与点击状态。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'sankey', + data: [ + { + values: [ + { + nodes: [ + { nodeName: "Agricultural 'waste'" }, + { nodeName: 'Bio-conversion' }, + { nodeName: 'Liquid' }, + { nodeName: 'Losses' }, + { nodeName: 'Solid' }, + { nodeName: 'Gas' }, + { nodeName: 'Biofuel imports' }, + { nodeName: 'Biomass imports' }, + { nodeName: 'Coal imports' }, + { nodeName: 'Coal' }, + { nodeName: 'Coal reserves' }, + { nodeName: 'District heating' }, + { nodeName: 'Industry' }, + { nodeName: 'Heating and cooling - commercial' }, + { nodeName: 'Heating and cooling - homes' }, + { nodeName: 'Electricity grid' }, + { nodeName: 'Over generation / exports' }, + { nodeName: 'H2 conversion' }, + { nodeName: 'Road transport' }, + { nodeName: 'Agriculture' }, + { nodeName: 'Rail transport' }, + { nodeName: 'Lighting & appliances - commercial' }, + { nodeName: 'Lighting & appliances - homes' }, + { nodeName: 'Gas imports' }, + { nodeName: 'Ngas' }, + { nodeName: 'Gas reserves' }, + { nodeName: 'Thermal generation' }, + { nodeName: 'Geothermal' }, + { nodeName: 'H2' }, + { nodeName: 'Hydro' }, + { nodeName: 'International shipping' }, + { nodeName: 'Domestic aviation' }, + { nodeName: 'International aviation' }, + { nodeName: 'National navigation' }, + { nodeName: 'Marine algae' }, + { nodeName: 'Nuclear' }, + { nodeName: 'Oil imports' }, + { nodeName: 'Oil' }, + { nodeName: 'Oil reserves' }, + { nodeName: 'Other waste' }, + { nodeName: 'Pumped heat' }, + { nodeName: 'Sun energy PV' }, + { nodeName: 'Sun energy Thermal' }, + { nodeName: 'Sun energy' }, + { nodeName: 'Tidal' }, + { nodeName: 'UK land based bioenergy' }, + { nodeName: 'Wave' }, + { nodeName: 'Wind' } + ], + links: [ + { source: 0, target: 1, value: 124.729 }, + { source: 1, target: 2, value: 0.597 }, + { source: 1, target: 3, value: 26.862 }, + { source: 1, target: 4, value: 280.322 }, + { source: 1, target: 5, value: 81.144 }, + { source: 6, target: 2, value: 35 }, + { source: 7, target: 4, value: 35 }, + { source: 8, target: 9, value: 11.606 }, + { source: 10, target: 9, value: 63.965 }, + { source: 9, target: 4, value: 75.571 }, + { source: 11, target: 12, value: 10.639 }, + { source: 11, target: 13, value: 22.505 }, + { source: 11, target: 14, value: 46.184 }, + { source: 15, target: 16, value: 104.453 }, + { source: 15, target: 14, value: 113.726 }, + { source: 15, target: 17, value: 27.14 }, + { source: 15, target: 12, value: 342.165 }, + { source: 15, target: 18, value: 37.797 }, + { source: 15, target: 19, value: 4.412 }, + { source: 15, target: 13, value: 40.858 }, + { source: 15, target: 3, value: 56.691 }, + { source: 15, target: 20, value: 7.863 }, + { source: 15, target: 21, value: 90.008 }, + { source: 15, target: 22, value: 93.494 }, + { source: 23, target: 24, value: 40.719 }, + { source: 25, target: 24, value: 82.233 }, + { source: 5, target: 13, value: 0.129 }, + { source: 5, target: 3, value: 1.401 }, + { source: 5, target: 26, value: 151.891 }, + { source: 5, target: 19, value: 2.096 }, + { source: 5, target: 12, value: 48.58 }, + { source: 27, target: 15, value: 7.013 }, + { source: 17, target: 28, value: 20.897 }, + { source: 17, target: 3, value: 6.242 }, + { source: 28, target: 18, value: 20.897 }, + { source: 29, target: 15, value: 6.995 }, + { source: 2, target: 12, value: 121.066 }, + { source: 2, target: 30, value: 128.69 }, + { source: 2, target: 18, value: 135.835 }, + { source: 2, target: 31, value: 14.458 }, + { source: 2, target: 32, value: 206.267 }, + { source: 2, target: 19, value: 3.64 }, + { source: 2, target: 33, value: 33.218 }, + { source: 2, target: 20, value: 4.413 }, + { source: 34, target: 1, value: 4.375 }, + { source: 24, target: 5, value: 122.952 }, + { source: 35, target: 26, value: 839.978 }, + { source: 36, target: 37, value: 504.287 }, + { source: 38, target: 37, value: 107.703 }, + { source: 37, target: 2, value: 611.99 }, + { source: 39, target: 4, value: 56.587 }, + { source: 39, target: 1, value: 77.81 }, + { source: 40, target: 14, value: 193.026 }, + { source: 40, target: 13, value: 70.672 }, + { source: 41, target: 15, value: 59.901 }, + { source: 42, target: 14, value: 19.263 }, + { source: 43, target: 42, value: 19.263 }, + { source: 43, target: 41, value: 59.901 }, + { source: 4, target: 19, value: 0.882 }, + { source: 4, target: 26, value: 400.12 }, + { source: 4, target: 12, value: 46.477 }, + { source: 26, target: 15, value: 525.531 }, + { source: 26, target: 3, value: 787.129 }, + { source: 26, target: 11, value: 79.329 }, + { source: 44, target: 15, value: 9.452 }, + { source: 45, target: 1, value: 182.01 }, + { source: 46, target: 15, value: 19.013 }, + { source: 47, target: 15, value: 289.366 } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + nodeAlign: 'justify', + nodeGap: 8, + nodeWidth: 10, + minNodeHeight: 4, + tooltip: { transitionDuration: 0 }, + title: { + text: 'How energy is converted or transmitted before being consumed or lost', + subtext: 'Data: Department of Energy & Climate Change via Tom Counsell', + subtextStyle: { fontSize: 12 } + }, + label: { visible: true, style: { fontSize: 10 } }, + node: { + state: { + hover: { stroke: '#333333' }, + selected: { fill: '#dddddd', stroke: '#333333', lineWidth: 1, brighter: 1, fillOpacity: 1 } + } + }, + link: { + state: { + hover: { fillOpacity: 1 }, + selected: { fill: '#dddddd', stroke: '#333333', lineWidth: 1, brighter: 1, fillOpacity: 1 } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'node'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + const hover = await seriesStates(page); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(h => (window.__nodeHover = h), hover); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + const selected = await seriesStates(page); + await page.evaluate(selected => { + if ( + !window.__nodeHover?.some(m => m.name === 'node' && m.states.hover > 0) || + !selected.some(m => m.name === 'node' && m.states.selected > 0) + ) + throw Error('桑基节点未悬停或选中'); + }, selected); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-selected-adjacency.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-selected-adjacency.mjs new file mode 100644 index 0000000000..2974bc9c22 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-selected-adjacency.mjs @@ -0,0 +1,654 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7f1852a1e9eec95f9efd + * 验证目的:点击桑基节点时仅高亮相邻节点与连接,空白点击恢复。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { + nodes: [ + { name: 'EPC', id: 0 }, + { name: 'ePDG', id: 1 }, + { name: 'Installation', id: 2 }, + { name: 'Misc OPEX', id: 3 }, + { name: 'Software', id: 4 }, + { name: 'Vendor Support', id: 5 }, + { name: 'HSS', id: 6 }, + { name: 'MME', id: 7 }, + { name: 'NFVI', id: 8 }, + { name: 'Network', id: 9 }, + { name: 'Power', id: 10 }, + { name: 'Servers', id: 11 }, + { name: 'Storage', id: 12 }, + { name: 'PCRF', id: 13 }, + { name: 'SAEGW-C', id: 14 }, + { name: 'SAEGW-U', id: 15 }, + { name: 'IMS', id: 16 }, + { name: 'IMS Core', id: 17 }, + { name: 'MMTEL-AS', id: 18 }, + { name: 'SBC', id: 19 }, + { name: 'RAN', id: 20 }, + { name: '4G PHY BBU', id: 21 }, + { name: 'Hardware', id: 22 }, + { name: '4G Radios', id: 23 }, + { name: '4G vBBU', id: 24 }, + { name: '5G PHY DU/CU', id: 25 }, + { name: '5G Radios', id: 26 }, + { name: '5G vCU', id: 27 }, + { name: '5G vDU', id: 28 }, + { name: 'Routers', id: 29 }, + { name: 'Aggregation Router', id: 30 }, + { name: 'Cell Site Router', id: 31 }, + { name: 'Core Router', id: 32 }, + { name: 'DC Edge Router', id: 33 }, + { name: 'X-Haul', id: 34 }, + { name: 'Backhaul', id: 35 }, + { name: 'Fiber', id: 36 }, + { name: 'Leased Line', id: 37 }, + { name: 'Microwave', id: 38 }, + { name: 'Front-haul', id: 39 }, + { name: 'Mid-haul', id: 40 } + ], + links: [ + { source: 0, target: 1, type: 'capex', value: 266480, id: 0 }, + { source: 1, target: 2, type: 'capex', value: 266480, id: 0 }, + { source: 0, target: 1, type: 'opex', value: 395705, id: 1 }, + { source: 1, target: 2, type: 'opex', value: 395705, id: 1 }, + { source: 0, target: 1, type: 'capex', value: 0, id: 2 }, + { source: 1, target: 3, type: 'capex', value: 0, id: 2 }, + { source: 0, target: 1, type: 'opex', value: 508173, id: 3 }, + { source: 1, target: 3, type: 'opex', value: 508173, id: 3 }, + { source: 0, target: 1, type: 'capex', value: 1011085, id: 4 }, + { source: 1, target: 4, type: 'capex', value: 1011085, id: 4 }, + { source: 0, target: 1, type: 'opex', value: 286040, id: 5 }, + { source: 1, target: 4, type: 'opex', value: 286040, id: 5 }, + { source: 0, target: 1, type: 'capex', value: 867898, id: 6 }, + { source: 1, target: 5, type: 'capex', value: 867898, id: 6 }, + { source: 0, target: 1, type: 'opex', value: 775430, id: 7 }, + { source: 1, target: 5, type: 'opex', value: 775430, id: 7 }, + { source: 0, target: 6, type: 'capex', value: 829901, id: 8 }, + { source: 6, target: 2, type: 'capex', value: 829901, id: 8 }, + { source: 0, target: 6, type: 'opex', value: 909286, id: 9 }, + { source: 6, target: 2, type: 'opex', value: 909286, id: 9 }, + { source: 0, target: 6, type: 'capex', value: 0, id: 10 }, + { source: 6, target: 3, type: 'capex', value: 0, id: 10 }, + { source: 0, target: 6, type: 'opex', value: 607037, id: 11 }, + { source: 6, target: 3, type: 'opex', value: 607037, id: 11 }, + { source: 0, target: 6, type: 'capex', value: 813737, id: 12 }, + { source: 6, target: 4, type: 'capex', value: 813737, id: 12 }, + { source: 0, target: 6, type: 'opex', value: 1091395, id: 13 }, + { source: 6, target: 4, type: 'opex', value: 1091395, id: 13 }, + { source: 0, target: 6, type: 'capex', value: 786918, id: 14 }, + { source: 6, target: 5, type: 'capex', value: 786918, id: 14 }, + { source: 0, target: 6, type: 'opex', value: 602743, id: 15 }, + { source: 6, target: 5, type: 'opex', value: 602743, id: 15 }, + { source: 0, target: 7, type: 'capex', value: 268511, id: 16 }, + { source: 7, target: 2, type: 'capex', value: 268511, id: 16 }, + { source: 0, target: 7, type: 'opex', value: 155255, id: 17 }, + { source: 7, target: 2, type: 'opex', value: 155255, id: 17 }, + { source: 0, target: 7, type: 'capex', value: 0, id: 18 }, + { source: 7, target: 3, type: 'capex', value: 0, id: 18 }, + { source: 0, target: 7, type: 'opex', value: 765295, id: 19 }, + { source: 7, target: 3, type: 'opex', value: 765295, id: 19 }, + { source: 0, target: 7, type: 'capex', value: 660240, id: 20 }, + { source: 7, target: 4, type: 'capex', value: 660240, id: 20 }, + { source: 0, target: 7, type: 'opex', value: 174476, id: 21 }, + { source: 7, target: 4, type: 'opex', value: 174476, id: 21 }, + { source: 0, target: 7, type: 'capex', value: 988223, id: 22 }, + { source: 7, target: 5, type: 'capex', value: 988223, id: 22 }, + { source: 0, target: 7, type: 'opex', value: 769696, id: 23 }, + { source: 7, target: 5, type: 'opex', value: 769696, id: 23 }, + { source: 0, target: 8, type: 'capex', value: 235713, id: 24 }, + { source: 8, target: 2, type: 'capex', value: 235713, id: 24 }, + { source: 0, target: 8, type: 'opex', value: 624912, id: 25 }, + { source: 8, target: 2, type: 'opex', value: 624912, id: 25 }, + { source: 0, target: 8, type: 'capex', value: 0, id: 26 }, + { source: 8, target: 3, type: 'capex', value: 0, id: 26 }, + { source: 0, target: 8, type: 'opex', value: 663440, id: 27 }, + { source: 8, target: 3, type: 'opex', value: 663440, id: 27 }, + { source: 0, target: 8, type: 'capex', value: 993004, id: 28 }, + { source: 8, target: 9, type: 'capex', value: 993004, id: 28 }, + { source: 0, target: 8, type: 'opex', value: 208134, id: 29 }, + { source: 8, target: 9, type: 'opex', value: 208134, id: 29 }, + { source: 0, target: 8, type: 'capex', value: 125927, id: 30 }, + { source: 8, target: 10, type: 'capex', value: 125927, id: 30 }, + { source: 0, target: 8, type: 'opex', value: 411321, id: 31 }, + { source: 8, target: 10, type: 'opex', value: 411321, id: 31 }, + { source: 0, target: 8, type: 'capex', value: 921691, id: 32 }, + { source: 8, target: 11, type: 'capex', value: 921691, id: 32 }, + { source: 0, target: 8, type: 'opex', value: 1076721, id: 33 }, + { source: 8, target: 11, type: 'opex', value: 1076721, id: 33 }, + { source: 0, target: 8, type: 'capex', value: 263806, id: 34 }, + { source: 8, target: 12, type: 'capex', value: 263806, id: 34 }, + { source: 0, target: 8, type: 'opex', value: 1061232, id: 35 }, + { source: 8, target: 12, type: 'opex', value: 1061232, id: 35 }, + { source: 0, target: 8, type: 'capex', value: 837451, id: 36 }, + { source: 8, target: 5, type: 'capex', value: 837451, id: 36 }, + { source: 0, target: 8, type: 'opex', value: 749956, id: 37 }, + { source: 8, target: 5, type: 'opex', value: 749956, id: 37 }, + { source: 0, target: 13, type: 'capex', value: 873115, id: 38 }, + { source: 13, target: 2, type: 'capex', value: 873115, id: 38 }, + { source: 0, target: 13, type: 'opex', value: 968112, id: 39 }, + { source: 13, target: 2, type: 'opex', value: 968112, id: 39 }, + { source: 0, target: 13, type: 'capex', value: 0, id: 40 }, + { source: 13, target: 3, type: 'capex', value: 0, id: 40 }, + { source: 0, target: 13, type: 'opex', value: 695514, id: 41 }, + { source: 13, target: 3, type: 'opex', value: 695514, id: 41 }, + { source: 0, target: 13, type: 'capex', value: 175091, id: 42 }, + { source: 13, target: 4, type: 'capex', value: 175091, id: 42 }, + { source: 0, target: 13, type: 'opex', value: 109021, id: 43 }, + { source: 13, target: 4, type: 'opex', value: 109021, id: 43 }, + { source: 0, target: 13, type: 'capex', value: 679308, id: 44 }, + { source: 13, target: 5, type: 'capex', value: 679308, id: 44 }, + { source: 0, target: 13, type: 'opex', value: 699599, id: 45 }, + { source: 13, target: 5, type: 'opex', value: 699599, id: 45 }, + { source: 0, target: 14, type: 'capex', value: 546335, id: 46 }, + { source: 14, target: 2, type: 'capex', value: 546335, id: 46 }, + { source: 0, target: 14, type: 'opex', value: 699368, id: 47 }, + { source: 14, target: 2, type: 'opex', value: 699368, id: 47 }, + { source: 0, target: 14, type: 'capex', value: 0, id: 48 }, + { source: 14, target: 3, type: 'capex', value: 0, id: 48 }, + { source: 0, target: 14, type: 'opex', value: 664315, id: 49 }, + { source: 14, target: 3, type: 'opex', value: 664315, id: 49 }, + { source: 0, target: 14, type: 'capex', value: 468705, id: 50 }, + { source: 14, target: 4, type: 'capex', value: 468705, id: 50 }, + { source: 0, target: 14, type: 'opex', value: 725414, id: 51 }, + { source: 14, target: 4, type: 'opex', value: 725414, id: 51 }, + { source: 0, target: 14, type: 'capex', value: 180385, id: 52 }, + { source: 14, target: 5, type: 'capex', value: 180385, id: 52 }, + { source: 0, target: 14, type: 'opex', value: 403680, id: 53 }, + { source: 14, target: 5, type: 'opex', value: 403680, id: 53 }, + { source: 0, target: 15, type: 'capex', value: 422335, id: 54 }, + { source: 15, target: 2, type: 'capex', value: 422335, id: 54 }, + { source: 0, target: 15, type: 'opex', value: 809964, id: 55 }, + { source: 15, target: 2, type: 'opex', value: 809964, id: 55 }, + { source: 0, target: 15, type: 'capex', value: 0, id: 56 }, + { source: 15, target: 3, type: 'capex', value: 0, id: 56 }, + { source: 0, target: 15, type: 'opex', value: 104445, id: 57 }, + { source: 15, target: 3, type: 'opex', value: 104445, id: 57 }, + { source: 0, target: 15, type: 'capex', value: 1013272, id: 58 }, + { source: 15, target: 4, type: 'capex', value: 1013272, id: 58 }, + { source: 0, target: 15, type: 'opex', value: 898483, id: 59 }, + { source: 15, target: 4, type: 'opex', value: 898483, id: 59 }, + { source: 0, target: 15, type: 'capex', value: 493227, id: 60 }, + { source: 15, target: 5, type: 'capex', value: 493227, id: 60 }, + { source: 0, target: 15, type: 'opex', value: 1049142, id: 61 }, + { source: 15, target: 5, type: 'opex', value: 1049142, id: 61 }, + { source: 16, target: 17, type: 'capex', value: 366229, id: 62 }, + { source: 17, target: 2, type: 'capex', value: 366229, id: 62 }, + { source: 16, target: 17, type: 'opex', value: 562622, id: 63 }, + { source: 17, target: 2, type: 'opex', value: 562622, id: 63 }, + { source: 16, target: 17, type: 'capex', value: 0, id: 64 }, + { source: 17, target: 3, type: 'capex', value: 0, id: 64 }, + { source: 16, target: 17, type: 'opex', value: 972746, id: 65 }, + { source: 17, target: 3, type: 'opex', value: 972746, id: 65 }, + { source: 16, target: 17, type: 'capex', value: 754003, id: 66 }, + { source: 17, target: 4, type: 'capex', value: 754003, id: 66 }, + { source: 16, target: 17, type: 'opex', value: 367255, id: 67 }, + { source: 17, target: 4, type: 'opex', value: 367255, id: 67 }, + { source: 16, target: 17, type: 'capex', value: 866169, id: 68 }, + { source: 17, target: 5, type: 'capex', value: 866169, id: 68 }, + { source: 16, target: 17, type: 'opex', value: 686324, id: 69 }, + { source: 17, target: 5, type: 'opex', value: 686324, id: 69 }, + { source: 16, target: 18, type: 'capex', value: 864480, id: 70 }, + { source: 18, target: 2, type: 'capex', value: 864480, id: 70 }, + { source: 16, target: 18, type: 'opex', value: 632619, id: 71 }, + { source: 18, target: 2, type: 'opex', value: 632619, id: 71 }, + { source: 16, target: 18, type: 'capex', value: 0, id: 72 }, + { source: 18, target: 3, type: 'capex', value: 0, id: 72 }, + { source: 16, target: 18, type: 'opex', value: 669312, id: 73 }, + { source: 18, target: 3, type: 'opex', value: 669312, id: 73 }, + { source: 16, target: 18, type: 'capex', value: 1080201, id: 74 }, + { source: 18, target: 4, type: 'capex', value: 1080201, id: 74 }, + { source: 16, target: 18, type: 'opex', value: 572439, id: 75 }, + { source: 18, target: 4, type: 'opex', value: 572439, id: 75 }, + { source: 16, target: 18, type: 'capex', value: 334047, id: 76 }, + { source: 18, target: 5, type: 'capex', value: 334047, id: 76 }, + { source: 16, target: 18, type: 'opex', value: 655790, id: 77 }, + { source: 18, target: 5, type: 'opex', value: 655790, id: 77 }, + { source: 16, target: 8, type: 'capex', value: 947459, id: 78 }, + { source: 8, target: 2, type: 'capex', value: 947459, id: 78 }, + { source: 16, target: 8, type: 'opex', value: 908187, id: 79 }, + { source: 8, target: 2, type: 'opex', value: 908187, id: 79 }, + { source: 16, target: 8, type: 'capex', value: 0, id: 80 }, + { source: 8, target: 3, type: 'capex', value: 0, id: 80 }, + { source: 16, target: 8, type: 'opex', value: 375858, id: 81 }, + { source: 8, target: 3, type: 'opex', value: 375858, id: 81 }, + { source: 16, target: 8, type: 'capex', value: 836180, id: 82 }, + { source: 8, target: 9, type: 'capex', value: 836180, id: 82 }, + { source: 16, target: 8, type: 'opex', value: 613911, id: 83 }, + { source: 8, target: 9, type: 'opex', value: 613911, id: 83 }, + { source: 16, target: 8, type: 'capex', value: 561127, id: 84 }, + { source: 8, target: 10, type: 'capex', value: 561127, id: 84 }, + { source: 16, target: 8, type: 'opex', value: 392066, id: 85 }, + { source: 8, target: 10, type: 'opex', value: 392066, id: 85 }, + { source: 16, target: 8, type: 'capex', value: 261411, id: 86 }, + { source: 8, target: 11, type: 'capex', value: 261411, id: 86 }, + { source: 16, target: 8, type: 'opex', value: 394224, id: 87 }, + { source: 8, target: 11, type: 'opex', value: 394224, id: 87 }, + { source: 16, target: 8, type: 'capex', value: 884487, id: 88 }, + { source: 8, target: 12, type: 'capex', value: 884487, id: 88 }, + { source: 16, target: 8, type: 'opex', value: 273804, id: 89 }, + { source: 8, target: 12, type: 'opex', value: 273804, id: 89 }, + { source: 16, target: 8, type: 'capex', value: 1049693, id: 90 }, + { source: 8, target: 5, type: 'capex', value: 1049693, id: 90 }, + { source: 16, target: 8, type: 'opex', value: 292792, id: 91 }, + { source: 8, target: 5, type: 'opex', value: 292792, id: 91 }, + { source: 16, target: 19, type: 'capex', value: 174100, id: 92 }, + { source: 19, target: 2, type: 'capex', value: 174100, id: 92 }, + { source: 16, target: 19, type: 'opex', value: 1067317, id: 93 }, + { source: 19, target: 2, type: 'opex', value: 1067317, id: 93 }, + { source: 16, target: 19, type: 'capex', value: 0, id: 94 }, + { source: 19, target: 3, type: 'capex', value: 0, id: 94 }, + { source: 16, target: 19, type: 'opex', value: 967728, id: 95 }, + { source: 19, target: 3, type: 'opex', value: 967728, id: 95 }, + { source: 16, target: 19, type: 'capex', value: 881901, id: 96 }, + { source: 19, target: 4, type: 'capex', value: 881901, id: 96 }, + { source: 16, target: 19, type: 'opex', value: 402069, id: 97 }, + { source: 19, target: 4, type: 'opex', value: 402069, id: 97 }, + { source: 16, target: 19, type: 'capex', value: 118887, id: 98 }, + { source: 19, target: 5, type: 'capex', value: 118887, id: 98 }, + { source: 16, target: 19, type: 'opex', value: 101042, id: 99 }, + { source: 19, target: 5, type: 'opex', value: 101042, id: 99 }, + { source: 20, target: 21, type: 'capex', value: 653954, id: 100 }, + { source: 21, target: 22, type: 'capex', value: 653954, id: 100 }, + { source: 20, target: 21, type: 'opex', value: 333188, id: 101 }, + { source: 21, target: 22, type: 'opex', value: 333188, id: 101 }, + { source: 20, target: 21, type: 'capex', value: 285457, id: 102 }, + { source: 21, target: 2, type: 'capex', value: 285457, id: 102 }, + { source: 20, target: 21, type: 'opex', value: 598430, id: 103 }, + { source: 21, target: 2, type: 'opex', value: 598430, id: 103 }, + { source: 20, target: 21, type: 'capex', value: 0, id: 104 }, + { source: 21, target: 3, type: 'capex', value: 0, id: 104 }, + { source: 20, target: 21, type: 'opex', value: 915046, id: 105 }, + { source: 21, target: 3, type: 'opex', value: 915046, id: 105 }, + { source: 20, target: 21, type: 'capex', value: 355344, id: 106 }, + { source: 21, target: 10, type: 'capex', value: 355344, id: 106 }, + { source: 20, target: 21, type: 'opex', value: 878333, id: 107 }, + { source: 21, target: 10, type: 'opex', value: 878333, id: 107 }, + { source: 20, target: 21, type: 'capex', value: 1081990, id: 108 }, + { source: 21, target: 4, type: 'capex', value: 1081990, id: 108 }, + { source: 20, target: 21, type: 'opex', value: 463959, id: 109 }, + { source: 21, target: 4, type: 'opex', value: 463959, id: 109 }, + { source: 20, target: 21, type: 'capex', value: 414656, id: 110 }, + { source: 21, target: 5, type: 'capex', value: 414656, id: 110 }, + { source: 20, target: 21, type: 'opex', value: 762247, id: 111 }, + { source: 21, target: 5, type: 'opex', value: 762247, id: 111 }, + { source: 20, target: 23, type: 'capex', value: 1076898, id: 112 }, + { source: 23, target: 22, type: 'capex', value: 1076898, id: 112 }, + { source: 20, target: 23, type: 'opex', value: 883601, id: 113 }, + { source: 23, target: 22, type: 'opex', value: 883601, id: 113 }, + { source: 20, target: 23, type: 'capex', value: 161972, id: 114 }, + { source: 23, target: 2, type: 'capex', value: 161972, id: 114 }, + { source: 20, target: 23, type: 'opex', value: 693171, id: 115 }, + { source: 23, target: 2, type: 'opex', value: 693171, id: 115 }, + { source: 20, target: 23, type: 'capex', value: 0, id: 116 }, + { source: 23, target: 3, type: 'capex', value: 0, id: 116 }, + { source: 20, target: 23, type: 'opex', value: 734033, id: 117 }, + { source: 23, target: 3, type: 'opex', value: 734033, id: 117 }, + { source: 20, target: 23, type: 'capex', value: 261237, id: 118 }, + { source: 23, target: 10, type: 'capex', value: 261237, id: 118 }, + { source: 20, target: 23, type: 'opex', value: 789371, id: 119 }, + { source: 23, target: 10, type: 'opex', value: 789371, id: 119 }, + { source: 20, target: 23, type: 'capex', value: 545683, id: 120 }, + { source: 23, target: 5, type: 'capex', value: 545683, id: 120 }, + { source: 20, target: 23, type: 'opex', value: 193733, id: 121 }, + { source: 23, target: 5, type: 'opex', value: 193733, id: 121 }, + { source: 20, target: 24, type: 'capex', value: 549311, id: 122 }, + { source: 24, target: 2, type: 'capex', value: 549311, id: 122 }, + { source: 20, target: 24, type: 'opex', value: 1063677, id: 123 }, + { source: 24, target: 2, type: 'opex', value: 1063677, id: 123 }, + { source: 20, target: 24, type: 'capex', value: 0, id: 124 }, + { source: 24, target: 3, type: 'capex', value: 0, id: 124 }, + { source: 20, target: 24, type: 'opex', value: 554298, id: 125 }, + { source: 24, target: 3, type: 'opex', value: 554298, id: 125 }, + { source: 20, target: 24, type: 'capex', value: 170444, id: 126 }, + { source: 24, target: 4, type: 'capex', value: 170444, id: 126 }, + { source: 20, target: 24, type: 'opex', value: 580447, id: 127 }, + { source: 24, target: 4, type: 'opex', value: 580447, id: 127 }, + { source: 20, target: 24, type: 'capex', value: 748527, id: 128 }, + { source: 24, target: 5, type: 'capex', value: 748527, id: 128 }, + { source: 20, target: 24, type: 'opex', value: 1073070, id: 129 }, + { source: 24, target: 5, type: 'opex', value: 1073070, id: 129 }, + { source: 20, target: 25, type: 'capex', value: 725681, id: 130 }, + { source: 25, target: 22, type: 'capex', value: 725681, id: 130 }, + { source: 20, target: 25, type: 'opex', value: 958227, id: 131 }, + { source: 25, target: 22, type: 'opex', value: 958227, id: 131 }, + { source: 20, target: 25, type: 'capex', value: 947389, id: 132 }, + { source: 25, target: 2, type: 'capex', value: 947389, id: 132 }, + { source: 20, target: 25, type: 'opex', value: 643894, id: 133 }, + { source: 25, target: 2, type: 'opex', value: 643894, id: 133 }, + { source: 20, target: 25, type: 'capex', value: 0, id: 134 }, + { source: 25, target: 3, type: 'capex', value: 0, id: 134 }, + { source: 20, target: 25, type: 'opex', value: 740700, id: 135 }, + { source: 25, target: 3, type: 'opex', value: 740700, id: 135 }, + { source: 20, target: 25, type: 'capex', value: 659567, id: 136 }, + { source: 25, target: 10, type: 'capex', value: 659567, id: 136 }, + { source: 20, target: 25, type: 'opex', value: 896960, id: 137 }, + { source: 25, target: 10, type: 'opex', value: 896960, id: 137 }, + { source: 20, target: 25, type: 'capex', value: 356342, id: 138 }, + { source: 25, target: 4, type: 'capex', value: 356342, id: 138 }, + { source: 20, target: 25, type: 'opex', value: 300453, id: 139 }, + { source: 25, target: 4, type: 'opex', value: 300453, id: 139 }, + { source: 20, target: 25, type: 'capex', value: 205078, id: 140 }, + { source: 25, target: 5, type: 'capex', value: 205078, id: 140 }, + { source: 20, target: 25, type: 'opex', value: 393739, id: 141 }, + { source: 25, target: 5, type: 'opex', value: 393739, id: 141 }, + { source: 20, target: 26, type: 'capex', value: 580461, id: 142 }, + { source: 26, target: 22, type: 'capex', value: 580461, id: 142 }, + { source: 20, target: 26, type: 'opex', value: 1056840, id: 143 }, + { source: 26, target: 22, type: 'opex', value: 1056840, id: 143 }, + { source: 20, target: 26, type: 'capex', value: 680491, id: 144 }, + { source: 26, target: 2, type: 'capex', value: 680491, id: 144 }, + { source: 20, target: 26, type: 'opex', value: 346085, id: 145 }, + { source: 26, target: 2, type: 'opex', value: 346085, id: 145 }, + { source: 20, target: 26, type: 'capex', value: 0, id: 146 }, + { source: 26, target: 3, type: 'capex', value: 0, id: 146 }, + { source: 20, target: 26, type: 'opex', value: 158893, id: 147 }, + { source: 26, target: 3, type: 'opex', value: 158893, id: 147 }, + { source: 20, target: 26, type: 'capex', value: 651999, id: 148 }, + { source: 26, target: 10, type: 'capex', value: 651999, id: 148 }, + { source: 20, target: 26, type: 'opex', value: 725290, id: 149 }, + { source: 26, target: 10, type: 'opex', value: 725290, id: 149 }, + { source: 20, target: 26, type: 'capex', value: 909947, id: 150 }, + { source: 26, target: 5, type: 'capex', value: 909947, id: 150 }, + { source: 20, target: 26, type: 'opex', value: 1055763, id: 151 }, + { source: 26, target: 5, type: 'opex', value: 1055763, id: 151 }, + { source: 20, target: 27, type: 'capex', value: 451275, id: 152 }, + { source: 27, target: 2, type: 'capex', value: 451275, id: 152 }, + { source: 20, target: 27, type: 'opex', value: 843917, id: 153 }, + { source: 27, target: 2, type: 'opex', value: 843917, id: 153 }, + { source: 20, target: 27, type: 'capex', value: 0, id: 154 }, + { source: 27, target: 3, type: 'capex', value: 0, id: 154 }, + { source: 20, target: 27, type: 'opex', value: 288484, id: 155 }, + { source: 27, target: 3, type: 'opex', value: 288484, id: 155 }, + { source: 20, target: 27, type: 'capex', value: 277201, id: 156 }, + { source: 27, target: 4, type: 'capex', value: 277201, id: 156 }, + { source: 20, target: 27, type: 'opex', value: 160747, id: 157 }, + { source: 27, target: 4, type: 'opex', value: 160747, id: 157 }, + { source: 20, target: 27, type: 'capex', value: 1040436, id: 158 }, + { source: 27, target: 5, type: 'capex', value: 1040436, id: 158 }, + { source: 20, target: 27, type: 'opex', value: 554122, id: 159 }, + { source: 27, target: 5, type: 'opex', value: 554122, id: 159 }, + { source: 20, target: 28, type: 'capex', value: 601965, id: 160 }, + { source: 28, target: 2, type: 'capex', value: 601965, id: 160 }, + { source: 20, target: 28, type: 'opex', value: 385598, id: 161 }, + { source: 28, target: 2, type: 'opex', value: 385598, id: 161 }, + { source: 20, target: 28, type: 'capex', value: 0, id: 162 }, + { source: 28, target: 3, type: 'capex', value: 0, id: 162 }, + { source: 20, target: 28, type: 'opex', value: 829306, id: 163 }, + { source: 28, target: 3, type: 'opex', value: 829306, id: 163 }, + { source: 20, target: 28, type: 'capex', value: 786184, id: 164 }, + { source: 28, target: 4, type: 'capex', value: 786184, id: 164 }, + { source: 20, target: 28, type: 'opex', value: 290542, id: 165 }, + { source: 28, target: 4, type: 'opex', value: 290542, id: 165 }, + { source: 20, target: 28, type: 'capex', value: 823352, id: 166 }, + { source: 28, target: 5, type: 'capex', value: 823352, id: 166 }, + { source: 20, target: 28, type: 'opex', value: 132898, id: 167 }, + { source: 28, target: 5, type: 'opex', value: 132898, id: 167 }, + { source: 20, target: 8, type: 'capex', value: 408016, id: 168 }, + { source: 8, target: 2, type: 'capex', value: 408016, id: 168 }, + { source: 20, target: 8, type: 'opex', value: 477229, id: 169 }, + { source: 8, target: 2, type: 'opex', value: 477229, id: 169 }, + { source: 20, target: 8, type: 'capex', value: 0, id: 170 }, + { source: 8, target: 3, type: 'capex', value: 0, id: 170 }, + { source: 20, target: 8, type: 'opex', value: 579901, id: 171 }, + { source: 8, target: 3, type: 'opex', value: 579901, id: 171 }, + { source: 20, target: 8, type: 'capex', value: 440690, id: 172 }, + { source: 8, target: 9, type: 'capex', value: 440690, id: 172 }, + { source: 20, target: 8, type: 'opex', value: 854642, id: 173 }, + { source: 8, target: 9, type: 'opex', value: 854642, id: 173 }, + { source: 20, target: 8, type: 'capex', value: 392053, id: 174 }, + { source: 8, target: 10, type: 'capex', value: 392053, id: 174 }, + { source: 20, target: 8, type: 'opex', value: 1046477, id: 175 }, + { source: 8, target: 10, type: 'opex', value: 1046477, id: 175 }, + { source: 20, target: 8, type: 'capex', value: 875521, id: 176 }, + { source: 8, target: 11, type: 'capex', value: 875521, id: 176 }, + { source: 20, target: 8, type: 'opex', value: 605256, id: 177 }, + { source: 8, target: 11, type: 'opex', value: 605256, id: 177 }, + { source: 20, target: 8, type: 'capex', value: 908072, id: 178 }, + { source: 8, target: 12, type: 'capex', value: 908072, id: 178 }, + { source: 20, target: 8, type: 'opex', value: 882152, id: 179 }, + { source: 8, target: 12, type: 'opex', value: 882152, id: 179 }, + { source: 20, target: 8, type: 'capex', value: 489117, id: 180 }, + { source: 8, target: 5, type: 'capex', value: 489117, id: 180 }, + { source: 20, target: 8, type: 'opex', value: 174482, id: 181 }, + { source: 8, target: 5, type: 'opex', value: 174482, id: 181 }, + { source: 29, target: 30, type: 'capex', value: 164438, id: 182 }, + { source: 30, target: 22, type: 'capex', value: 164438, id: 182 }, + { source: 29, target: 30, type: 'opex', value: 909142, id: 183 }, + { source: 30, target: 22, type: 'opex', value: 909142, id: 183 }, + { source: 29, target: 30, type: 'capex', value: 192038, id: 184 }, + { source: 30, target: 2, type: 'capex', value: 192038, id: 184 }, + { source: 29, target: 30, type: 'opex', value: 872092, id: 185 }, + { source: 30, target: 2, type: 'opex', value: 872092, id: 185 }, + { source: 29, target: 30, type: 'capex', value: 0, id: 186 }, + { source: 30, target: 3, type: 'capex', value: 0, id: 186 }, + { source: 29, target: 30, type: 'opex', value: 176746, id: 187 }, + { source: 30, target: 3, type: 'opex', value: 176746, id: 187 }, + { source: 29, target: 30, type: 'capex', value: 147151, id: 188 }, + { source: 30, target: 10, type: 'capex', value: 147151, id: 188 }, + { source: 29, target: 30, type: 'opex', value: 212508, id: 189 }, + { source: 30, target: 10, type: 'opex', value: 212508, id: 189 }, + { source: 29, target: 30, type: 'capex', value: 1013010, id: 190 }, + { source: 30, target: 4, type: 'capex', value: 1013010, id: 190 }, + { source: 29, target: 30, type: 'opex', value: 413604, id: 191 }, + { source: 30, target: 4, type: 'opex', value: 413604, id: 191 }, + { source: 29, target: 30, type: 'capex', value: 788170, id: 192 }, + { source: 30, target: 5, type: 'capex', value: 788170, id: 192 }, + { source: 29, target: 30, type: 'opex', value: 874060, id: 193 }, + { source: 30, target: 5, type: 'opex', value: 874060, id: 193 }, + { source: 29, target: 31, type: 'capex', value: 787684, id: 194 }, + { source: 31, target: 22, type: 'capex', value: 787684, id: 194 }, + { source: 29, target: 31, type: 'opex', value: 291938, id: 195 }, + { source: 31, target: 22, type: 'opex', value: 291938, id: 195 }, + { source: 29, target: 31, type: 'capex', value: 644602, id: 196 }, + { source: 31, target: 2, type: 'capex', value: 644602, id: 196 }, + { source: 29, target: 31, type: 'opex', value: 783713, id: 197 }, + { source: 31, target: 2, type: 'opex', value: 783713, id: 197 }, + { source: 29, target: 31, type: 'capex', value: 0, id: 198 }, + { source: 31, target: 3, type: 'capex', value: 0, id: 198 }, + { source: 29, target: 31, type: 'opex', value: 187534, id: 199 }, + { source: 31, target: 3, type: 'opex', value: 187534, id: 199 }, + { source: 29, target: 31, type: 'capex', value: 423492, id: 200 }, + { source: 31, target: 10, type: 'capex', value: 423492, id: 200 }, + { source: 29, target: 31, type: 'opex', value: 491269, id: 201 }, + { source: 31, target: 10, type: 'opex', value: 491269, id: 201 }, + { source: 29, target: 31, type: 'capex', value: 230389, id: 202 }, + { source: 31, target: 4, type: 'capex', value: 230389, id: 202 }, + { source: 29, target: 31, type: 'opex', value: 220966, id: 203 }, + { source: 31, target: 4, type: 'opex', value: 220966, id: 203 }, + { source: 29, target: 31, type: 'capex', value: 231059, id: 204 }, + { source: 31, target: 5, type: 'capex', value: 231059, id: 204 }, + { source: 29, target: 31, type: 'opex', value: 765826, id: 205 }, + { source: 31, target: 5, type: 'opex', value: 765826, id: 205 }, + { source: 29, target: 32, type: 'capex', value: 707538, id: 206 }, + { source: 32, target: 22, type: 'capex', value: 707538, id: 206 }, + { source: 29, target: 32, type: 'opex', value: 152531, id: 207 }, + { source: 32, target: 22, type: 'opex', value: 152531, id: 207 }, + { source: 29, target: 32, type: 'capex', value: 245398, id: 208 }, + { source: 32, target: 2, type: 'capex', value: 245398, id: 208 }, + { source: 29, target: 32, type: 'opex', value: 292262, id: 209 }, + { source: 32, target: 2, type: 'opex', value: 292262, id: 209 }, + { source: 29, target: 32, type: 'capex', value: 0, id: 210 }, + { source: 32, target: 3, type: 'capex', value: 0, id: 210 }, + { source: 29, target: 32, type: 'opex', value: 994458, id: 211 }, + { source: 32, target: 3, type: 'opex', value: 994458, id: 211 }, + { source: 29, target: 32, type: 'capex', value: 651120, id: 212 }, + { source: 32, target: 10, type: 'capex', value: 651120, id: 212 }, + { source: 29, target: 32, type: 'opex', value: 992413, id: 213 }, + { source: 32, target: 10, type: 'opex', value: 992413, id: 213 }, + { source: 29, target: 32, type: 'capex', value: 1048656, id: 214 }, + { source: 32, target: 4, type: 'capex', value: 1048656, id: 214 }, + { source: 29, target: 32, type: 'opex', value: 157538, id: 215 }, + { source: 32, target: 4, type: 'opex', value: 157538, id: 215 }, + { source: 29, target: 32, type: 'capex', value: 542675, id: 216 }, + { source: 32, target: 5, type: 'capex', value: 542675, id: 216 }, + { source: 29, target: 32, type: 'opex', value: 885053, id: 217 }, + { source: 32, target: 5, type: 'opex', value: 885053, id: 217 }, + { source: 29, target: 33, type: 'capex', value: 513022, id: 218 }, + { source: 33, target: 22, type: 'capex', value: 513022, id: 218 }, + { source: 29, target: 33, type: 'opex', value: 165670, id: 219 }, + { source: 33, target: 22, type: 'opex', value: 165670, id: 219 }, + { source: 29, target: 33, type: 'capex', value: 328677, id: 220 }, + { source: 33, target: 2, type: 'capex', value: 328677, id: 220 }, + { source: 29, target: 33, type: 'opex', value: 945520, id: 221 }, + { source: 33, target: 2, type: 'opex', value: 945520, id: 221 }, + { source: 29, target: 33, type: 'capex', value: 0, id: 222 }, + { source: 33, target: 3, type: 'capex', value: 0, id: 222 }, + { source: 29, target: 33, type: 'opex', value: 790034, id: 223 }, + { source: 33, target: 3, type: 'opex', value: 790034, id: 223 }, + { source: 29, target: 33, type: 'capex', value: 553342, id: 224 }, + { source: 33, target: 10, type: 'capex', value: 553342, id: 224 }, + { source: 29, target: 33, type: 'opex', value: 753783, id: 225 }, + { source: 33, target: 10, type: 'opex', value: 753783, id: 225 }, + { source: 29, target: 33, type: 'capex', value: 500764, id: 226 }, + { source: 33, target: 4, type: 'capex', value: 500764, id: 226 }, + { source: 29, target: 33, type: 'opex', value: 546620, id: 227 }, + { source: 33, target: 4, type: 'opex', value: 546620, id: 227 }, + { source: 29, target: 33, type: 'capex', value: 710407, id: 228 }, + { source: 33, target: 5, type: 'capex', value: 710407, id: 228 }, + { source: 29, target: 33, type: 'opex', value: 484295, id: 229 }, + { source: 33, target: 5, type: 'opex', value: 484295, id: 229 }, + { source: 34, target: 35, type: 'capex', value: 217877, id: 230 }, + { source: 35, target: 36, type: 'capex', value: 217877, id: 230 }, + { source: 34, target: 35, type: 'opex', value: 765848, id: 231 }, + { source: 35, target: 36, type: 'opex', value: 765848, id: 231 }, + { source: 34, target: 35, type: 'capex', value: 929306, id: 232 }, + { source: 35, target: 37, type: 'capex', value: 929306, id: 232 }, + { source: 34, target: 35, type: 'opex', value: 393679, id: 233 }, + { source: 35, target: 37, type: 'opex', value: 393679, id: 233 }, + { source: 34, target: 35, type: 'capex', value: 975866, id: 234 }, + { source: 35, target: 38, type: 'capex', value: 975866, id: 234 }, + { source: 34, target: 35, type: 'opex', value: 466235, id: 235 }, + { source: 35, target: 38, type: 'opex', value: 466235, id: 235 }, + { source: 34, target: 39, type: 'capex', value: 298517, id: 236 }, + { source: 39, target: 36, type: 'capex', value: 298517, id: 236 }, + { source: 34, target: 39, type: 'opex', value: 1042236, id: 237 }, + { source: 39, target: 36, type: 'opex', value: 1042236, id: 237 }, + { source: 34, target: 40, type: 'capex', value: 106439, id: 238 }, + { source: 40, target: 36, type: 'capex', value: 106439, id: 238 }, + { source: 34, target: 40, type: 'opex', value: 827545, id: 239 }, + { source: 40, target: 36, type: 'opex', value: 827545, id: 239 }, + { source: 34, target: 40, type: 'capex', value: 895263, id: 240 }, + { source: 40, target: 37, type: 'capex', value: 895263, id: 240 }, + { source: 34, target: 40, type: 'opex', value: 685034, id: 241 }, + { source: 40, target: 37, type: 'opex', value: 685034, id: 241 }, + { source: 34, target: 40, type: 'capex', value: 478800, id: 242 }, + { source: 40, target: 38, type: 'capex', value: 478800, id: 242 }, + { source: 34, target: 40, type: 'opex', value: 614346, id: 243 }, + { source: 40, target: 38, type: 'opex', value: 614346, id: 243 } + ] + } + ]; + const spec = { + type: 'sankey', + data: [{ name: 'data', values: data }], + categoryField: 'name', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + colorField: 'type', + direction: 'vertical', + nodeAlign: 'start', + nodeGap: 2, + nodeWidth: '10%', + minNodeHeight: 4, + label: { visible: true, style: { fontSize: 12 }, position: 'inside-middle' }, + node: { state: { hover: { fill: 'red' }, blur: { fill: '#e8e8e8', fillOpacity: 0.15 } } }, + link: { + style: { + fill: data => { + if (data.type ?? data.datum.type === 'opex') { + return 'red'; + } + if (data.type ?? data.datum.type === 'capex') { + return 'yellow'; + } + return 'blue'; + }, + fillOpacity: 0.1 + }, + state: { hover: { fillOpacity: 0.4 }, blur: { fill: '#e8e8e8' } } + }, + emphasis: { enable: true, trigger: 'selected', effect: 'adjacency' }, + tooltip: { transitionDuration: 0 }, + title: { + text: 'Experimental Sankey diagram visualizes expenses flow', + subtext: 'Public data source', + subtextStyle: { fontSize: 12 } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const records = []; + for (const index of [0, -1, 1]) { + if (index < 0) await page.mouse.click(790, 590); + else { + const p = await interactionTarget(page, 'node', index); + await page.mouse.click(p.x, p.y); + } + await page.mouse.move(950, 750); + await interactionFrame(page); + records.push( + await page.evaluate(index => { + const s = window.__visualChart.getChart().getAllSeries()[0], + nodes = s + .getMarks() + .find(m => m.name === 'node') + .getGraphics(), + links = s + .getMarks() + .find(m => m.name === 'link') + .getGraphics(); + return { + index, + key: index < 0 ? null : nodes[index].context.data[0].key, + nodes: nodes.map(g => ({ key: g.context.data[0].key, states: g.currentStates })), + links: links.map(g => ({ + source: g.context.data[0].source, + target: g.context.data[0].target, + states: g.currentStates + })) + }; + }, index) + ); + } + await page.evaluate(r => (window.__adjacency = r), records); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const rows = window.__adjacency; + if (rows?.length !== 3) throw Error('缺少邻接状态过程'); + for (const r of rows) { + if (r.index < 0) { + if ([...r.nodes, ...r.links].some(g => g.states.some(s => ['selected', 'blur'].includes(s)))) + throw Error('邻接高亮未清除'); + continue; + } + const neighbors = new Set([r.key]); + for (const l of r.links) { + const match = l.source === r.key || l.target === r.key; + if (match) { + neighbors.add(l.source); + neighbors.add(l.target); + } + if (!l.states.includes(match ? 'selected' : 'blur')) throw Error('链接邻接状态错误'); + } + for (const n of r.nodes) + if (!n.states.includes(neighbors.has(n.key) ? 'selected' : 'blur')) throw Error('节点邻接状态错误'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-vertical.mjs b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-vertical.mjs new file mode 100644 index 0000000000..599e14aac3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sankey/sankey-vertical.mjs @@ -0,0 +1,568 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e045e + * 验证目的:纵向桑基图节点和连接布局。 + * 保留条件:padding, categoryField, valueField, sourceField, targetField, direction, nodeAlign, nodeGap, nodeWidth, minNodeHeight, label, node, link;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'sankey', + padding: 5, + data: [ + { + name: 'data', + values: [ + { + nodes: [ + { + nodeName: "Agricultural 'waste'" + }, + { + nodeName: 'Bio-conversion' + }, + { + nodeName: 'Liquid' + }, + { + nodeName: 'Losses' + }, + { + nodeName: 'Solid' + }, + { + nodeName: 'Gas' + }, + { + nodeName: 'Biofuel imports' + }, + { + nodeName: 'Biomass imports' + }, + { + nodeName: 'Coal imports' + }, + { + nodeName: 'Coal' + }, + { + nodeName: 'Coal reserves' + }, + { + nodeName: 'District heating' + }, + { + nodeName: 'Industry' + }, + { + nodeName: 'Heating and cooling - commercial' + }, + { + nodeName: 'Heating and cooling - homes' + }, + { + nodeName: 'Electricity grid' + }, + { + nodeName: 'Over generation / exports' + }, + { + nodeName: 'H2 conversion' + }, + { + nodeName: 'Road transport' + }, + { + nodeName: 'Agriculture' + }, + { + nodeName: 'Rail transport' + }, + { + nodeName: 'Lighting & appliances - commercial' + }, + { + nodeName: 'Lighting & appliances - homes' + }, + { + nodeName: 'Gas imports' + }, + { + nodeName: 'Ngas' + }, + { + nodeName: 'Gas reserves' + }, + { + nodeName: 'Thermal generation' + }, + { + nodeName: 'Geothermal' + }, + { + nodeName: 'H2' + }, + { + nodeName: 'Hydro' + }, + { + nodeName: 'International shipping' + }, + { + nodeName: 'Domestic aviation' + }, + { + nodeName: 'International aviation' + }, + { + nodeName: 'National navigation' + }, + { + nodeName: 'Marine algae' + }, + { + nodeName: 'Nuclear' + }, + { + nodeName: 'Oil imports' + }, + { + nodeName: 'Oil' + }, + { + nodeName: 'Oil reserves' + }, + { + nodeName: 'Other waste' + }, + { + nodeName: 'Pumped heat' + }, + { + nodeName: 'Sunlight PV' + }, + { + nodeName: 'Sunlight thermal' + }, + { + nodeName: 'Sunlight' + }, + { + nodeName: 'Tidal' + }, + { + nodeName: 'UK land based bioenergy' + }, + { + nodeName: 'Wave' + }, + { + nodeName: 'Wind' + } + ], + links: [ + { + source: 0, + target: 1, + value: 124.729 + }, + { + source: 1, + target: 2, + value: 0.597 + }, + { + source: 1, + target: 3, + value: 26.862 + }, + { + source: 1, + target: 4, + value: 280.322 + }, + { + source: 1, + target: 5, + value: 81.144 + }, + { + source: 6, + target: 2, + value: 35 + }, + { + source: 7, + target: 4, + value: 35 + }, + { + source: 8, + target: 9, + value: 11.606 + }, + { + source: 10, + target: 9, + value: 63.965 + }, + { + source: 9, + target: 4, + value: 75.571 + }, + { + source: 11, + target: 12, + value: 10.639 + }, + { + source: 11, + target: 13, + value: 22.505 + }, + { + source: 11, + target: 14, + value: 46.184 + }, + { + source: 15, + target: 16, + value: 104.453 + }, + { + source: 15, + target: 14, + value: 113.726 + }, + { + source: 15, + target: 17, + value: 27.14 + }, + { + source: 15, + target: 12, + value: 342.165 + }, + { + source: 15, + target: 18, + value: 37.797 + }, + { + source: 15, + target: 19, + value: 4.412 + }, + { + source: 15, + target: 13, + value: 40.858 + }, + { + source: 15, + target: 3, + value: 56.691 + }, + { + source: 15, + target: 20, + value: 7.863 + }, + { + source: 15, + target: 21, + value: 90.008 + }, + { + source: 15, + target: 22, + value: 93.494 + }, + { + source: 23, + target: 24, + value: 40.719 + }, + { + source: 25, + target: 24, + value: 82.233 + }, + { + source: 5, + target: 13, + value: 0.129 + }, + { + source: 5, + target: 3, + value: 1.401 + }, + { + source: 5, + target: 26, + value: 151.891 + }, + { + source: 5, + target: 19, + value: 2.096 + }, + { + source: 5, + target: 12, + value: 48.58 + }, + { + source: 27, + target: 15, + value: 7.013 + }, + { + source: 17, + target: 28, + value: 20.897 + }, + { + source: 17, + target: 3, + value: 6.242 + }, + { + source: 28, + target: 18, + value: 20.897 + }, + { + source: 29, + target: 15, + value: 6.995 + }, + { + source: 2, + target: 12, + value: 121.066 + }, + { + source: 2, + target: 30, + value: 128.69 + }, + { + source: 2, + target: 18, + value: 135.835 + }, + { + source: 2, + target: 31, + value: 14.458 + }, + { + source: 2, + target: 32, + value: 206.267 + }, + { + source: 2, + target: 19, + value: 3.64 + }, + { + source: 2, + target: 33, + value: 33.218 + }, + { + source: 2, + target: 20, + value: 4.413 + }, + { + source: 34, + target: 1, + value: 4.375 + }, + { + source: 24, + target: 5, + value: 122.952 + }, + { + source: 35, + target: 26, + value: 839.978 + }, + { + source: 36, + target: 37, + value: 504.287 + }, + { + source: 38, + target: 37, + value: 107.703 + }, + { + source: 37, + target: 2, + value: 611.99 + }, + { + source: 39, + target: 4, + value: 56.587 + }, + { + source: 39, + target: 1, + value: 77.81 + }, + { + source: 40, + target: 14, + value: 193.026 + }, + { + source: 40, + target: 13, + value: 70.672 + }, + { + source: 41, + target: 15, + value: 59.901 + }, + { + source: 42, + target: 14, + value: 19.263 + }, + { + source: 43, + target: 42, + value: 19.263 + }, + { + source: 43, + target: 41, + value: 59.901 + }, + { + source: 4, + target: 19, + value: 0.882 + }, + { + source: 4, + target: 26, + value: 400.12 + }, + { + source: 4, + target: 12, + value: 46.477 + }, + { + source: 26, + target: 15, + value: 525.531 + }, + { + source: 26, + target: 3, + value: 787.129 + }, + { + source: 26, + target: 11, + value: 79.329 + }, + { + source: 44, + target: 15, + value: 9.452 + }, + { + source: 45, + target: 1, + value: 182.01 + }, + { + source: 46, + target: 15, + value: 19.013 + }, + { + source: 47, + target: 15, + value: 289.366 + } + ] + } + ] + } + ], + categoryField: 'nodeName', + valueField: 'value', + sourceField: 'source', + targetField: 'target', + direction: 'vertical', + nodeAlign: 'start', + nodeGap: 2, + nodeWidth: '30%', + minNodeHeight: 4, + label: { + visible: true, + style: { + fontSize: 12 + }, + position: 'inside-middle' + }, + node: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: '#dddddd', + stroke: '#333333', + strokeWidth: 1, + brighter: 1, + fillOpacity: 1 + } + } + }, + link: { + style: { + fill: '#333333', + fillOpacity: 0.2 + }, + state: { + hover: { + fillOpacity: 1 + }, + selected: { + fill: '#dddddd', + stroke: '#333333', + strokeWidth: 1, + brighter: 1, + fillOpacity: 1 + } + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-shape.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-shape.mjs new file mode 100644 index 0000000000..aa46fcf765 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-shape.mjs @@ -0,0 +1,207 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0421 + * 验证目的:散点形状与线性大小编码。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'scatter', + data: [ + { + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + shapeField: 'type', + shape: { + type: 'ordinal', + range: ['star', 'triangleLeft', 'diamond'] + }, + sizeField: 'size', + size: { + type: 'linear', + range: [5, 25] + }, + point: { + state: { + hover: { + scaleX: 1.2, + scaleY: 1.2 + } + } + }, + axes: [ + { + orient: 'left', + range: { + min: 0 + }, + type: 'linear' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { + visible: true, + style: { + text: 'Title' + } + }, + item: { + visible: true + } + } + ], + direction: 'horizontal' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-size.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-size.mjs new file mode 100644 index 0000000000..a3d4228f0b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-ordinal-size.mjs @@ -0,0 +1,202 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0420 + * 验证目的:散点离散大小编码。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'scatter', + data: [ + { + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + sizeField: 'type', + size: { + type: 'ordinal', + range: [10, 30, 50] + }, + point: { + state: { + hover: { + scaleX: 1.2, + scaleY: 1.2 + } + } + }, + axes: [ + { + orient: 'left', + range: { + min: 0 + }, + type: 'linear' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { + visible: true, + style: { + text: 'Title' + } + }, + item: { + visible: true + } + } + ], + direction: 'horizontal' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-radial-gradient.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-radial-gradient.mjs new file mode 100644 index 0000000000..c4c4f69e28 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-radial-gradient.mjs @@ -0,0 +1,258 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f0352a1e9eec95f9eba + * 验证目的:散点径向渐变填充。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'scatter', + data: { + values: [ + { + type: 'A', + x: 9, + y: 81, + r: 63 + }, + { + type: 'A', + x: 98, + y: 5, + r: 89 + }, + { + type: 'A', + x: 51, + y: 50, + r: 73 + }, + { + type: 'A', + x: 41, + y: 22, + r: 14 + }, + { + type: 'A', + x: 58, + y: 24, + r: 20 + }, + { + type: 'A', + x: 78, + y: 37, + r: 34 + }, + { + type: 'A', + x: 55, + y: 56, + r: 53 + }, + { + type: 'A', + x: 18, + y: 45, + r: 70 + }, + { + type: 'A', + x: 42, + y: 44, + r: 28 + }, + { + type: 'A', + x: 3, + y: 52, + r: 59 + }, + { + type: 'A', + x: 31, + y: 18, + r: 97 + }, + { + type: 'A', + x: 79, + y: 91, + r: 63 + }, + { + type: 'A', + x: 93, + y: 23, + r: 23 + }, + { + type: 'A', + x: 44, + y: 83, + r: 22 + }, + { + type: 'B', + x: 42, + y: 38, + r: 20 + }, + { + type: 'B', + x: 6, + y: 18, + r: 1 + }, + { + type: 'B', + x: 1, + y: 93, + r: 55 + }, + { + type: 'B', + x: 57, + y: 2, + r: 90 + }, + { + type: 'B', + x: 80, + y: 76, + r: 22 + }, + { + type: 'B', + x: 11, + y: 74, + r: 96 + }, + { + type: 'B', + x: 88, + y: 56, + r: 10 + }, + { + type: 'B', + x: 30, + y: 47, + r: 49 + }, + { + type: 'B', + x: 57, + y: 62, + r: 98 + }, + { + type: 'B', + x: 4, + y: 16, + r: 16 + }, + { + type: 'B', + x: 46, + y: 10, + r: 11 + }, + { + type: 'B', + x: 22, + y: 87, + r: 89 + }, + { + type: 'B', + x: 57, + y: 91, + r: 82 + }, + { + type: 'B', + x: 45, + y: 15, + r: 98 + } + ] + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + sizeField: 'r', + size: { + type: 'linear', + range: [6, 35] + }, + color: [ + { + gradient: 'radial', + x0: 0.5, + y0: 0.5, + r0: 0, + x1: 0.5, + y1: 1, + r1: 0.7, + stops: [ + { + offset: 0, + color: 'rgba(255,255,255,0.5)' + }, + { + offset: 1, + color: '#6690F2' + } + ] + }, + { + gradient: 'radial', + x0: 0.5, + y0: 0.5, + r0: 0, + x1: 0.5, + y1: 1, + r1: 0.7, + stops: [ + { + offset: 0, + color: 'rgba(255,255,255,0.5)' + }, + { + offset: 1, + color: '#FFDC83' + } + ] + } + ], + axes: [ + { + type: 'linear', + orient: 'left', + min: -10 + }, + { + type: 'linear', + orient: 'bottom', + domainLine: { + visible: true + } + } + ], + legends: { + visible: true + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-shape-callback.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-shape-callback.mjs new file mode 100644 index 0000000000..bf949a0aad --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-shape-callback.mjs @@ -0,0 +1,190 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0425 + * 验证目的:散点形状大小回调编码。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const data = [ + { + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + } + ]; + const spec = { + type: 'scatter', + data: data, + xField: 'x', + yField: 'y', + seriesField: 'type', + size: data => Math.sqrt(data['size'] / 100, 2), + shape: () => 'star', + point: { + state: { + hover: { + scaleX: 1.2, + scaleY: 1.2 + } + } + }, + axes: [ + { orient: 'left', range: { min: 0 }, type: 'linear' }, + { orient: 'bottom', label: { visible: true }, type: 'band' } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { + visible: true, + style: { + text: 'Title' + } + }, + item: { + visible: true + } + } + ], + direction: 'horizontal' + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-size-callback.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-size-callback.mjs new file mode 100644 index 0000000000..82f0457fee --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-size-callback.mjs @@ -0,0 +1,189 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0424 + * 验证目的:反向坐标轴与散点大小回调。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const data = [ + { + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + } + ]; + const spec = { + type: 'scatter', + data: data, + xField: 'x', + yField: 'y', + seriesField: 'type', + size: data => Math.sqrt(data['size'] / 10, 2), + point: { + state: { + hover: { + scaleX: 1.2, + scaleY: 1.2 + } + } + }, + axes: [ + { orient: 'left', range: { min: 0 }, type: 'linear', inverse: true }, + { orient: 'bottom', label: { visible: true }, type: 'band' } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { + visible: true, + style: { + text: 'Title' + } + }, + item: { + visible: true + } + } + ], + direction: 'horizontal' + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-symbol.mjs b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-symbol.mjs new file mode 100644 index 0000000000..0dd451decd --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/scatter/scatter-symbol.mjs @@ -0,0 +1,35 @@ +import { verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:散点位置、大小及符号(scatter)。 + * 图表类型:scatter。 + * 关键配置:sizeField、size、point.style.symbolType。 + * 场景条件:固定三个数值坐标和三种大小,使用 diamond 符号。 + * 最终检查:核对编码 spec、有效绘制与最终截图。 + * 覆盖边界:不覆盖全部符号、重叠和采样策略。 + */ +export default { + createSpec() { + // 使用数值坐标和大小通道覆盖散点绘制。 + return { + type: 'scatter', + data: { + id: 'data', + values: [ + { x: 1, y: 4, size: 12 }, + { x: 3, y: 2, size: 24 }, + { x: 5, y: 7, size: 36 } + ] + }, + xField: 'x', + yField: 'y', + sizeField: 'size', + size: [12, 36], + point: { style: { symbolType: 'diamond' } } + }; + }, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/sunburst/sunburst-gap.mjs b/packages/vchart/__tests__/visual/cases/charts/sunburst/sunburst-gap.mjs new file mode 100644 index 0000000000..263dff40f9 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/sunburst/sunburst-gap.mjs @@ -0,0 +1,191 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e045f + * 验证目的:旭日图分层间隙和径向标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const duration = 1000; + const easing = 'cubicInOut'; + const data = [ + { + name: 'Company A', + children: [ + { + name: 'Department A', + children: [ + { + name: 'Group 1', + value: 1 + }, + { + name: 'Group 2', + value: 2 + } + ] + }, + { + name: 'Department B', + children: [ + { + name: 'Group 3', + children: [ + { + name: '员工1', + value: 2 + }, + { + name: '员工2', + value: 1 + } + ] + } + ] + } + ] + }, + { + name: 'Company B', + children: [ + { + name: 'Department C', + children: [ + { + name: 'Group 4', + value: 4 + }, + { + name: 'Group 5', + value: 5 + } + ] + }, + { + name: 'Department D', + children: [ + { + name: 'Group 6', + value: 6 + } + ] + } + ] + }, + { + name: 'Company C', + children: [ + { + name: 'Department E', + children: [ + { + name: 'Group 7', + value: 7 + }, + { + name: 'Group 8', + value: 8 + } + ] + }, + { + name: 'Department F', + children: [ + { + name: 'Group 9', + value: 9 + } + ] + } + ] + }, + { + name: 'Company D', + children: [ + { + name: 'Department G', + value: 10 + }, + { + name: 'Department H', + value: 5 + } + ] + } + ]; + const spec = { + type: 'sunburst', + offsetX: 0, + offsetY: 0, + categoryField: 'name', + valueField: 'value', + outerRadius: 1, + endAngle: 180, + labelAutoVisible: { + enable: true, + circumference: 5 + }, + drill: true, + labelLayout: { + align: 'center', + rotate: 'radial', + offset: 0 + }, + gap: [5, 10], + sunburst: { + style: { + visible: true, + fillOpacity: datum => { + return datum.isLeaf ? 0.4 : 0.8; + } + } + }, + label: { + style: { + fillOpacity: datum => { + return datum.isLeaf ? 0.4 : 0.8; + } + } + }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + }, + data: [ + { + id: 'data', + values: data + } + ], + animation: true, + animationUpdate: { + easing: easing, + duration: duration + }, + animationExit: { + easing: easing, + duration: duration + }, + animationEnter: { + easing: easing, + duration: duration + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page, { animation: false }); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-drill.mjs b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-drill.mjs new file mode 100644 index 0000000000..755104b157 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-drill.mjs @@ -0,0 +1,147 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65af99db06085f008cf8f8a3 + * 迁移边界:源未录制鼠标;补充实际下钻动作验证 drill:true,不宣称重放历史录制。 + * 验证目的:来源层级图下钻与空白回退的路径和实际布局。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'treemap', + drill: true, + data: [ + { + id: 'data', + values: [ + { + name: 'Second', + children: [ + { + name: 'B1', + children: [ + { name: 'B11', value: 100 }, + { name: 'B12', value: 100 } + ] + }, + { name: 'B2', value: 98 }, + { name: 'B3', value: 56 } + ] + }, + { + name: 'First', + children: [ + { + name: 'A1', + value: 100, + children: [ + { name: 'A1-1', value: 60 }, + { name: 'A1-2', value: 40 } + ] + }, + { name: 'A2', value: 60 }, + { name: 'A3', value: 30 } + ] + }, + { + name: 'Third', + children: [ + { name: 'C1', value: 335 }, + { name: 'C2', value: 148 }, + { name: 'C3', value: 126 }, + { name: 'C4', value: 26 } + ] + }, + { + name: 'Fourth', + children: [ + { name: 'D1', value: 415 }, + { name: 'D2', value: 148 }, + { name: 'D3', value: 89 }, + { name: 'D4', value: 64 }, + { name: 'D5', value: 16 } + ] + }, + { + name: 'Fifth', + children: [ + { name: 'E1', value: 687 }, + { name: 'E2', value: 148 } + ] + } + ] + } + ], + aspectRatio: 0.7, + categoryField: 'name', + valueField: 'value', + legends: { visible: true }, + tooltip: { + transitionDuration: 0, + mark: { + title: { + value: datum => { + return datum?.datum?.map(data => data.name).join('/'); + } + } + } + }, + nonLeaf: { visible: true }, + nonLeafLabel: { position: 'top', visible: true, style: {} }, + label: { visible: true } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__drillEvents = []; + window.__hierarchyBefore = JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + ); + c.on('drill', e => window.__drillEvents.push({ type: e.value.type, path: [...e.value.path] })); + }); + const p = await interactionTarget(page, 'leaf', 1); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + await page.evaluate( + () => + (window.__hierarchyAfter = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.size]) + )) + ); + await page.mouse.click(790, 590); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const e = window.__drillEvents; + if ( + e?.length !== 2 || + !e[0].path.length || + e[1].path.length >= e[0].path.length || + window.__hierarchyBefore === window.__hierarchyAfter + ) + throw Error('层级下钻/回退没有改变路径与布局'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-hierarchy.mjs b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-hierarchy.mjs new file mode 100644 index 0000000000..0bd0c85eb1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-hierarchy.mjs @@ -0,0 +1,411 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f2352a1e9eec95f9f18 + * 验证目的:矩形树图的层次数据与标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:验证源层次布局、标题和标签的静态最终状态。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'treemap', + data: [ + { + id: 'data', + values: [ + { + name: 'query', + children: [ + { + name: 'methods', + children: [ + { name: 'add', value: 593 }, + { name: 'and', value: 330 }, + { name: 'average', value: 287 }, + { name: 'count', value: 277 }, + { name: 'distinct', value: 292 }, + { name: 'div', value: 595 }, + { name: 'eq', value: 594 }, + { name: 'fn', value: 460 }, + { name: 'gt', value: 603 }, + { name: 'gte', value: 625 }, + { name: 'iff', value: 748 }, + { name: 'isa', value: 461 }, + { name: 'lt', value: 597 }, + { name: 'lte', value: 619 }, + { name: 'max', value: 283 }, + { name: 'min', value: 283 }, + { name: 'mod', value: 591 }, + { name: 'mul', value: 603 }, + { name: 'neq', value: 599 }, + { name: 'not', value: 386 }, + { name: 'or', value: 323 }, + { name: 'orderby', value: 307 }, + { name: 'range', value: 772 }, + { name: 'select', value: 296 }, + { name: 'stddev', value: 363 }, + { name: 'sub', value: 600 }, + { name: 'sum', value: 280 }, + { name: 'update', value: 307 }, + { name: 'variance', value: 335 }, + { name: 'where', value: 299 }, + { name: 'xor', value: 354 }, + { name: '_', value: 264 } + ] + }, + { name: 'AggregateExpression', value: 1616 }, + { name: 'And', value: 1027 }, + { name: 'Arithmetic', value: 3891 }, + { name: 'Average', value: 891 }, + { name: 'BinaryExpression', value: 2893 }, + { name: 'Comparison', value: 5103 }, + { name: 'CompositeExpression', value: 3677 }, + { name: 'Count', value: 781 }, + { name: 'DateUtil', value: 4141 }, + { name: 'Distinct', value: 933 }, + { name: 'Expression', value: 5130 }, + { name: 'ExpressionIterator', value: 3617 }, + { name: 'Fn', value: 3240 }, + { name: 'If', value: 2732 }, + { name: 'IsA', value: 2039 }, + { name: 'Literal', value: 1214 }, + { name: 'Match', value: 3748 }, + { name: 'Maximum', value: 843 }, + { name: 'Minimum', value: 843 }, + { name: 'Not', value: 1554 }, + { name: 'Or', value: 970 }, + { name: 'Query', value: 13896 }, + { name: 'Range', value: 1594 }, + { name: 'StringUtil', value: 4130 }, + { name: 'Sum', value: 791 }, + { name: 'Variable', value: 1124 }, + { name: 'Variance', value: 1876 }, + { name: 'Xor', value: 1101 } + ] + }, + { + name: 'util', + children: [ + { + name: 'palette', + children: [ + { name: 'ColorPalette', value: 6367 }, + { name: 'Palette', value: 1229 }, + { name: 'ShapePalette', value: 2059 }, + { name: 'valuePalette', value: 2291 } + ] + }, + { + name: 'math', + children: [ + { name: 'DenseMatrix', value: 3165 }, + { name: 'IMatrix', value: 2815 }, + { name: 'SparseMatrix', value: 3366 } + ] + }, + { + name: 'heap', + children: [ + { name: 'FibonacciHeap', value: 9354 }, + { name: 'HeapNode', value: 1233 } + ] + }, + { name: 'Arrays', value: 8258 }, + { name: 'Colors', value: 10001 }, + { name: 'Dates', value: 8217 }, + { name: 'Displays', value: 12555 }, + { name: 'Filter', value: 2324 }, + { name: 'Geometry', value: 10993 }, + { name: 'IEvaluable', value: 335 }, + { name: 'IPredicate', value: 383 }, + { name: 'IValueProxy', value: 874 }, + { name: 'Maths', value: 17705 }, + { name: 'Orientation', value: 1486 }, + { name: 'Property', value: 5559 }, + { name: 'Shapes', value: 19118 }, + { name: 'Sort', value: 6887 }, + { name: 'Stats', value: 6557 }, + { name: 'Strings', value: 22026 } + ] + }, + { + name: 'animate', + children: [ + { + name: 'interpolate', + children: [ + { name: 'ArrayInterpolator', value: 1983 }, + { name: 'ColorInterpolator', value: 2047 }, + { name: 'DateInterpolator', value: 1375 }, + { name: 'Interpolator', value: 8746 }, + { name: 'MatrixInterpolator', value: 2202 }, + { name: 'NumberInterpolator', value: 1382 }, + { name: 'ObjectInterpolator', value: 1629 }, + { name: 'PointInterpolator', value: 1675 }, + { name: 'RectangleInterpolator', value: 2042 } + ] + }, + { name: 'Easing', value: 17010 }, + { name: 'FunctionSequence', value: 5842 }, + { name: 'ISchedulable', value: 1041 }, + { name: 'Parallel', value: 5176 }, + { name: 'Pause', value: 449 }, + { name: 'Scheduler', value: 5593 }, + { name: 'Sequence', value: 5534 }, + { name: 'Transition', value: 9201 }, + { name: 'Transitioner', value: 19975 }, + { name: 'TransitionEvent', value: 1116 }, + { name: 'Tween', value: 6006 } + ] + }, + { + name: 'scale', + children: [ + { name: 'IScaleMap', value: 2105 }, + { name: 'LinearScale', value: 1316 }, + { name: 'LogScale', value: 3151 }, + { name: 'OrdinalScale', value: 3770 }, + { name: 'QuantileScale', value: 2435 }, + { name: 'QuantitativeScale', value: 4839 }, + { name: 'RootScale', value: 1756 }, + { name: 'Scale', value: 4268 }, + { name: 'ScaleType', value: 1821 }, + { name: 'TimeScale', value: 5833 } + ] + }, + { + name: 'physics', + children: [ + { name: 'DragForce', value: 1082 }, + { name: 'GravityForce', value: 1336 }, + { name: 'IForce', value: 319 }, + { name: 'NBodyForce', value: 10498 }, + { name: 'Particle', value: 2822 }, + { name: 'Simulation', value: 9983 }, + { name: 'Spring', value: 2213 }, + { name: 'SpringForce', value: 1681 } + ] + }, + { + name: 'data', + children: [ + { + name: 'converters', + children: [ + { name: 'Converters', value: 721 }, + { name: 'DelimitedTextConverter', value: 4294 }, + { name: 'GraphMLConverter', value: 9800 }, + { name: 'IDataConverter', value: 1314 }, + { name: 'JSONConverter', value: 2220 } + ] + }, + { name: 'DataField', value: 1759 }, + { name: 'DataSchema', value: 2165 }, + { name: 'DataSet', value: 586 }, + { name: 'DataSource', value: 3331 }, + { name: 'DataTable', value: 772 }, + { name: 'DataUtil', value: 3322 } + ] + }, + { + name: 'vis', + children: [ + { + name: 'controls', + children: [ + { name: 'AnchorControl', value: 2138 }, + { name: 'ClickControl', value: 3824 }, + { name: 'Control', value: 1353 }, + { name: 'ControlList', value: 4665 }, + { name: 'DragControl', value: 2649 }, + { name: 'ExpandControl', value: 2832 }, + { name: 'HoverControl', value: 4896 }, + { name: 'IControl', value: 763 }, + { name: 'PanZoomControl', value: 5222 }, + { name: 'SelectionControl', value: 7862 }, + { name: 'TooltipControl', value: 8435 } + ] + }, + { + name: 'operator', + children: [ + { + name: 'layout', + children: [ + { name: 'AxisLayout', value: 6725 }, + { name: 'BundledEdgeRouter', value: 3727 }, + { name: 'CircleLayout', value: 9317 }, + { name: 'CirclePackingLayout', value: 12003 }, + { name: 'DendrogramLayout', value: 4853 }, + { name: 'ForceDirectedLayout', value: 8411 }, + { name: 'IcicleTreeLayout', value: 4864 }, + { name: 'IndentedTreeLayout', value: 3174 }, + { name: 'Layout', value: 7881 }, + { name: 'NodeLinkTreeLayout', value: 12870 }, + { name: 'PieLayout', value: 2728 }, + { name: 'RadialTreeLayout', value: 12348 }, + { name: 'RandomLayout', value: 870 }, + { name: 'StackedAreaLayout', value: 9121 }, + { name: 'TreeMapLayout', value: 9191 } + ] + }, + { + name: 'encoder', + children: [ + { name: 'ColorEncoder', value: 3179 }, + { name: 'Encoder', value: 4060 }, + { name: 'PropertyEncoder', value: 4138 }, + { name: 'ShapeEncoder', value: 1690 }, + { name: 'valueEncoder', value: 1830 } + ] + }, + { + name: 'distortion', + children: [ + { name: 'BifocalDistortion', value: 4461 }, + { name: 'Distortion', value: 6314 }, + { name: 'FisheyeDistortion', value: 3444 } + ] + }, + { + name: 'filter', + children: [ + { name: 'FisheyeTreeFilter', value: 5219 }, + { name: 'GraphDistanceFilter', value: 3165 }, + { name: 'VisibilityFilter', value: 3509 } + ] + }, + { + name: 'label', + children: [ + { name: 'Labeler', value: 9956 }, + { name: 'RadialLabeler', value: 3899 }, + { name: 'StackedAreaLabeler', value: 3202 } + ] + }, + { name: 'IOperator', value: 1286 }, + { name: 'Operator', value: 2490 }, + { name: 'OperatorList', value: 5248 }, + { name: 'OperatorSequence', value: 4190 }, + { name: 'OperatorSwitch', value: 2581 }, + { name: 'SortOperator', value: 2023 } + ] + }, + { + name: 'data', + children: [ + { + name: 'render', + children: [ + { name: 'ArrowType', value: 698 }, + { name: 'EdgeRenderer', value: 5569 }, + { name: 'IRenderer', value: 353 }, + { name: 'ShapeRenderer', value: 2247 } + ] + }, + { name: 'Data', value: 20544 }, + { name: 'DataList', value: 19788 }, + { name: 'DataSprite', value: 10349 }, + { name: 'EdgeSprite', value: 3301 }, + { name: 'NodeSprite', value: 19382 }, + { name: 'ScaleBinding', value: 11275 }, + { name: 'Tree', value: 7147 }, + { name: 'TreeBuilder', value: 9930 } + ] + }, + { + name: 'axis', + children: [ + { name: 'Axes', value: 1302 }, + { name: 'Axis', value: 24593 }, + { name: 'AxisGridLine', value: 652 }, + { name: 'AxisLabel', value: 636 }, + { name: 'CartesianAxes', value: 6703 } + ] + }, + { + name: 'events', + children: [ + { name: 'DataEvent', value: 2313 }, + { name: 'SelectionEvent', value: 1880 }, + { name: 'TooltipEvent', value: 1701 }, + { name: 'VisualizationEvent', value: 1117 } + ] + }, + { + name: 'legend', + children: [ + { name: 'Legend', value: 20859 }, + { name: 'LegendItem', value: 4614 }, + { name: 'LegendRange', value: 10530 } + ] + }, + { name: 'Visualization', value: 16540 } + ] + }, + { + name: 'display', + children: [ + { name: 'DirtySprite', value: 8833 }, + { name: 'LineSprite', value: 1732 }, + { name: 'RectSprite', value: 3623 }, + { name: 'TextSprite', value: 10066 } + ] + }, + { + name: 'analytics', + children: [ + { + name: 'graph', + children: [ + { name: 'BetweennessCentrality', value: 3534 }, + { name: 'LinkDistance', value: 5731 }, + { name: 'MaxFlowMinCut', value: 7840 }, + { name: 'ShortestPaths', value: 5914 }, + { name: 'SpanningTree', value: 3416 } + ] + }, + { + name: 'cluster', + children: [ + { name: 'AgglomerativeCluster', value: 3938 }, + { name: 'CommunityStructure', value: 3812 }, + { name: 'HierarchicalCluster', value: 6714 }, + { name: 'MergeEdge', value: 743 } + ] + }, + { + name: 'optimization', + children: [{ name: 'AspectRatioBanker', value: 7074 }] + } + ] + }, + { name: 'flex', children: [{ name: 'FlareVis', value: 4116 }] } + ] + } + ], + categoryField: 'name', + valueField: 'value', + label: { + visible: true, + style: { + fontSize: 12 + } + }, + title: { + visible: true, + text: 'The software class hierarchy of the Flare visualization toolkit' + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'label'); + await verifyRendered(page, 'title'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-nonleaf-label.mjs b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-nonleaf-label.mjs new file mode 100644 index 0000000000..92b6d9eebb --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/treemap/treemap-nonleaf-label.mjs @@ -0,0 +1,515 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb799997cc3d008de5b40d + * 验证目的:矩形树图父节点显示与标签格式化。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const richTextConfig = text => [ + { + text, + fontSize: 10, + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontSize: 10, + fill: 'green' + } + ]; + const spec = { + type: 'treemap', + width: 800, + data: [ + { + id: 'data', + values: [ + { + name: 'query', + children: [ + { + name: 'methods', + children: [ + { name: 'add', value: 593 }, + { name: 'and', value: 330 }, + { name: 'average', value: 287 }, + { name: 'count', value: 277 }, + { name: 'distinct', value: 292 }, + { name: 'div', value: 595 }, + { name: 'eq', value: 594 }, + { name: 'fn', value: 460 }, + { name: 'gt', value: 603 }, + { name: 'gte', value: 625 }, + { name: 'iff', value: 748 }, + { name: 'isa', value: 461 }, + { name: 'lt', value: 597 }, + { name: 'lte', value: 619 }, + { name: 'max', value: 283 }, + { name: 'min', value: 283 }, + { name: 'mod', value: 591 }, + { name: 'mul', value: 603 }, + { name: 'neq', value: 599 }, + { name: 'not', value: 386 }, + { name: 'or', value: 323 }, + { name: 'orderby', value: 307 }, + { name: 'range', value: 772 }, + { name: 'select', value: 296 }, + { name: 'stddev', value: 363 }, + { name: 'sub', value: 600 }, + { name: 'sum', value: 280 }, + { name: 'update', value: 307 }, + { name: 'variance', value: 335 }, + { name: 'where', value: 299 }, + { name: 'xor', value: 354 }, + { name: '_', value: 264 } + ] + }, + { name: 'AggregateExpression', value: 1616 }, + { name: 'And', value: 1027 }, + { name: 'Arithmetic', value: 3891 }, + { name: 'Average', value: 891 }, + { name: 'BinaryExpression', value: 2893 }, + { name: 'Comparison', value: 5103 }, + { name: 'CompositeExpression', value: 3677 }, + { name: 'Count', value: 781 }, + { name: 'DateUtil', value: 4141 }, + { name: 'Distinct', value: 933 }, + { name: 'Expression', value: 5130 }, + { name: 'ExpressionIterator', value: 3617 }, + { name: 'Fn', value: 3240 }, + { name: 'If', value: 2732 }, + { name: 'IsA', value: 2039 }, + { name: 'Literal', value: 1214 }, + { name: 'Match', value: 3748 }, + { name: 'Maximum', value: 843 }, + { name: 'Minimum', value: 843 }, + { name: 'Not', value: 1554 }, + { name: 'Or', value: 970 }, + { name: 'Query', value: 13896 }, + { name: 'Range', value: 1594 }, + { name: 'StringUtil', value: 4130 }, + { name: 'Sum', value: 791 }, + { name: 'Variable', value: 1124 }, + { name: 'Variance', value: 1876 }, + { name: 'Xor', value: 1101 } + ] + }, + { + name: 'util', + children: [ + { + name: 'palette', + children: [ + { name: 'ColorPalette', value: 6367 }, + { name: 'Palette', value: 1229 }, + { name: 'ShapePalette', value: 2059 }, + { name: 'valuePalette', value: 2291 } + ] + }, + { + name: 'math', + children: [ + { name: 'DenseMatrix', value: 3165 }, + { name: 'IMatrix', value: 2815 }, + { name: 'SparseMatrix', value: 3366 } + ] + }, + { + name: 'heap', + children: [ + { name: 'FibonacciHeap', value: 9354 }, + { name: 'HeapNode', value: 1233 } + ] + }, + { name: 'Arrays', value: 8258 }, + { name: 'Colors', value: 10001 }, + { name: 'Dates', value: 8217 }, + { name: 'Displays', value: 12555 }, + { name: 'Filter', value: 2324 }, + { name: 'Geometry', value: 10993 }, + { name: 'IEvaluable', value: 335 }, + { name: 'IPredicate', value: 383 }, + { name: 'IValueProxy', value: 874 }, + { name: 'Maths', value: 17705 }, + { name: 'Orientation', value: 1486 }, + { name: 'Property', value: 5559 }, + { name: 'Shapes', value: 19118 }, + { name: 'Sort', value: 6887 }, + { name: 'Stats', value: 6557 }, + { name: 'Strings', value: 22026 } + ] + }, + { + name: 'animate', + children: [ + { + name: 'interpolate', + children: [ + { name: 'ArrayInterpolator', value: 1983 }, + { name: 'ColorInterpolator', value: 2047 }, + { name: 'DateInterpolator', value: 1375 }, + { name: 'Interpolator', value: 8746 }, + { name: 'MatrixInterpolator', value: 2202 }, + { name: 'NumberInterpolator', value: 1382 }, + { name: 'ObjectInterpolator', value: 1629 }, + { name: 'PointInterpolator', value: 1675 }, + { name: 'RectangleInterpolator', value: 2042 } + ] + }, + { name: 'Easing', value: 17010 }, + { name: 'FunctionSequence', value: 5842 }, + { name: 'ISchedulable', value: 1041 }, + { name: 'Parallel', value: 5176 }, + { name: 'Pause', value: 449 }, + { name: 'Scheduler', value: 5593 }, + { name: 'Sequence', value: 5534 }, + { name: 'Transition', value: 9201 }, + { name: 'Transitioner', value: 19975 }, + { name: 'TransitionEvent', value: 1116 }, + { name: 'Tween', value: 6006 } + ] + }, + { + name: 'scale', + children: [ + { name: 'IScaleMap', value: 2105 }, + { name: 'LinearScale', value: 1316 }, + { name: 'LogScale', value: 3151 }, + { name: 'OrdinalScale', value: 3770 }, + { name: 'QuantileScale', value: 2435 }, + { name: 'QuantitativeScale', value: 4839 }, + { name: 'RootScale', value: 1756 }, + { name: 'Scale', value: 4268 }, + { name: 'ScaleType', value: 1821 }, + { name: 'TimeScale', value: 5833 } + ] + }, + { + name: 'physics', + children: [ + { name: 'DragForce', value: 1082 }, + { name: 'GravityForce', value: 1336 }, + { name: 'IForce', value: 319 }, + { name: 'NBodyForce', value: 10498 }, + { name: 'Particle', value: 2822 }, + { name: 'Simulation', value: 9983 }, + { name: 'Spring', value: 2213 }, + { name: 'SpringForce', value: 1681 } + ] + }, + { + name: 'data', + children: [ + { + name: 'converters', + children: [ + { name: 'Converters', value: 721 }, + { name: 'DelimitedTextConverter', value: 4294 }, + { name: 'GraphMLConverter', value: 9800 }, + { name: 'IDataConverter', value: 1314 }, + { name: 'JSONConverter', value: 2220 } + ] + }, + { name: 'DataField', value: 1759 }, + { name: 'DataSchema', value: 2165 }, + { name: 'DataSet', value: 586 }, + { name: 'DataSource', value: 3331 }, + { name: 'DataTable', value: 772 }, + { name: 'DataUtil', value: 3322 } + ] + }, + { + name: 'vis', + children: [ + { + name: 'controls', + children: [ + { name: 'AnchorControl', value: 2138 }, + { name: 'ClickControl', value: 3824 }, + { name: 'Control', value: 1353 }, + { name: 'ControlList', value: 4665 }, + { name: 'DragControl', value: 2649 }, + { name: 'ExpandControl', value: 2832 }, + { name: 'HoverControl', value: 4896 }, + { name: 'IControl', value: 763 }, + { name: 'PanZoomControl', value: 5222 }, + { name: 'SelectionControl', value: 7862 }, + { name: 'TooltipControl', value: 8435 } + ] + }, + { + name: 'operator', + children: [ + { + name: 'layout', + children: [ + { name: 'AxisLayout', value: 6725 }, + { name: 'BundledEdgeRouter', value: 3727 }, + { name: 'CircleLayout', value: 9317 }, + { name: 'CirclePackingLayout', value: 12003 }, + { name: 'DendrogramLayout', value: 4853 }, + { name: 'ForceDirectedLayout', value: 8411 }, + { name: 'IcicleTreeLayout', value: 4864 }, + { name: 'IndentedTreeLayout', value: 3174 }, + { name: 'Layout', value: 7881 }, + { name: 'NodeLinkTreeLayout', value: 12870 }, + { name: 'PieLayout', value: 2728 }, + { name: 'RadialTreeLayout', value: 12348 }, + { name: 'RandomLayout', value: 870 }, + { name: 'StackedAreaLayout', value: 9121 }, + { name: 'TreeMapLayout', value: 9191 } + ] + }, + { + name: 'encoder', + children: [ + { name: 'ColorEncoder', value: 3179 }, + { name: 'Encoder', value: 4060 }, + { name: 'PropertyEncoder', value: 4138 }, + { name: 'ShapeEncoder', value: 1690 }, + { name: 'valueEncoder', value: 1830 } + ] + }, + { + name: 'distortion', + children: [ + { name: 'BifocalDistortion', value: 4461 }, + { name: 'Distortion', value: 6314 }, + { name: 'FisheyeDistortion', value: 3444 } + ] + }, + { + name: 'filter', + children: [ + { name: 'FisheyeTreeFilter', value: 5219 }, + { name: 'GraphDistanceFilter', value: 3165 }, + { name: 'VisibilityFilter', value: 3509 } + ] + }, + { + name: 'label', + children: [ + { name: 'Labeler', value: 9956 }, + { name: 'RadialLabeler', value: 3899 }, + { name: 'StackedAreaLabeler', value: 3202 } + ] + }, + { name: 'IOperator', value: 1286 }, + { name: 'Operator', value: 2490 }, + { name: 'OperatorList', value: 5248 }, + { name: 'OperatorSequence', value: 4190 }, + { name: 'OperatorSwitch', value: 2581 }, + { name: 'SortOperator', value: 2023 } + ] + }, + { + name: 'data', + children: [ + { + name: 'render', + children: [ + { name: 'ArrowType', value: 698 }, + { name: 'EdgeRenderer', value: 5569 }, + { name: 'IRenderer', value: 353 }, + { name: 'ShapeRenderer', value: 2247 } + ] + }, + { name: 'Data', value: 20544 }, + { name: 'DataList', value: 19788 }, + { name: 'DataSprite', value: 10349 }, + { name: 'EdgeSprite', value: 3301 }, + { name: 'NodeSprite', value: 19382 }, + { name: 'ScaleBinding', value: 11275 }, + { name: 'Tree', value: 7147 }, + { name: 'TreeBuilder', value: 9930 } + ] + }, + { + name: 'axis', + children: [ + { name: 'Axes', value: 1302 }, + { name: 'Axis', value: 24593 }, + { name: 'AxisGridLine', value: 652 }, + { name: 'AxisLabel', value: 636 }, + { name: 'CartesianAxes', value: 6703 } + ] + }, + { + name: 'events', + children: [ + { name: 'DataEvent', value: 2313 }, + { name: 'SelectionEvent', value: 1880 }, + { name: 'TooltipEvent', value: 1701 }, + { name: 'VisualizationEvent', value: 1117 } + ] + }, + { + name: 'legend', + children: [ + { name: 'Legend', value: 20859 }, + { name: 'LegendItem', value: 4614 }, + { name: 'LegendRange', value: 10530 } + ] + }, + { name: 'Visualization', value: 16540 } + ] + }, + { + name: 'display', + children: [ + { name: 'DirtySprite', value: 8833 }, + { name: 'LineSprite', value: 1732 }, + { name: 'RectSprite', value: 3623 }, + { name: 'TextSprite', value: 10066 } + ] + }, + { + name: 'analytics', + children: [ + { + name: 'graph', + children: [ + { name: 'BetweennessCentrality', value: 3534 }, + { name: 'LinkDistance', value: 5731 }, + { name: 'MaxFlowMinCut', value: 7840 }, + { name: 'ShortestPaths', value: 5914 }, + { name: 'SpanningTree', value: 3416 } + ] + }, + { + name: 'cluster', + children: [ + { name: 'AgglomerativeCluster', value: 3938 }, + { name: 'CommunityStructure', value: 3812 }, + { name: 'HierarchicalCluster', value: 6714 }, + { name: 'MergeEdge', value: 743 } + ] + }, + { + name: 'optimization', + children: [{ name: 'AspectRatioBanker', value: 7074 }] + } + ] + }, + { name: 'flex', children: [{ name: 'FlareVis', value: 4116 }] } + ] + } + ], + legends: { + title: { + visible: true, + text: { + type: 'text', + text: 111 + } + }, + orient: 'right', + data: items => { + return items.map(item => { + item.value = item.label; + return item; + }); + }, + item: { + label: { + formatMethod: text => { + return { + type: 'rich', + text: [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red' + }, + { + text: '_', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green' + } + ] + }; + } + }, + value: { + space: 20, + formatMethod: text => { + return { + type: 'rich', + text: richTextConfig(`${text}`) + }; + } + } + } + }, + categoryField: 'name', + valueField: 'value', + label: { + visible: true, + formatMethod: text => { + return { + type: 'rich', + text: richTextConfig(text) + }; + }, + style: { + ellipsis: true, + maxHeight: 20 + } + }, + nonLeaf: { + visible: true + }, + nonLeafLabel: { + visible: true, + formatMethod: text => { + return { + type: 'rich', + text: richTextConfig(text) + }; + }, + position: 'top', + padding: 30, + style: { + x: data => { + return data.labelRect?.x0 + 4; + }, + textAlign: 'left', + text: data => [data.name, data.value] + } + }, + tooltip: { + mark: { + title: { + value: data => { + return data?.datum?.map(data => data.name).join('/'); + } + } + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-absolute-label.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-absolute-label.mjs new file mode 100644 index 0000000000..514bcbe6df --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-absolute-label.mjs @@ -0,0 +1,142 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0445 + * 验证目的:瀑布图绝对值堆叠标签与字段总计。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + seriesField: 'type', + data: [ + { + id: 'id0', + values: [ + { + x: 'Subtotal', + total: true, + start: 100, + value: 100 + }, + { + x: '0', + y: 20, + type: 'A' + }, + { + x: '0', + y: 11, + type: 'B' + }, + { + x: '1', + y: 20, + type: 'A' + }, + { + x: '1', + y: 20, + type: 'B' + }, + { + x: 'Subtotal1', + total: true, + collect: 3 + }, + { + x: '2', + y: -100, + type: 'A' + }, + { + x: '2', + y: -80, + type: 'B' + }, + { + x: '3', + y: -20, + type: 'A' + }, + { + x: '3', + y: -20, + type: 'B' + }, + { + x: 'Subtotal2', + total: true, + collect: 3 + }, + { + x: '4', + y: 20, + type: 'A' + }, + { + x: '4', + y: 20, + type: 'B' + }, + { + x: '5', + y: 20, + type: 'A' + }, + { + x: '5', + y: 20, + type: 'B' + }, + { + x: 'Total', + total: true, + collect: 3 + } + ] + } + ], + series: [ + { + type: 'waterfall', + dataIndex: 0, + xField: 'x', + yField: 'y', + seriesField: 'type', + total: { + type: 'field', + tagField: 'total', + startField: 'start', + valueField: 'value', + collectCountField: 'collect' + }, + stackLabel: { + valueType: 'absolute' + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band', + paddingInner: 0.4 + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-horizontal-total.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-horizontal-total.mjs new file mode 100644 index 0000000000..d8e6138b66 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-horizontal-total.mjs @@ -0,0 +1,141 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0442 + * 验证目的:横向瀑布图字段总计与累计连接。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + seriesField: 'type', + data: [ + { + id: 'id0', + values: [ + { + x: 'Subtotal', + total: true, + start: 100, + value: 100 + }, + { + x: '0', + y: 20, + type: 'A' + }, + { + x: '0', + y: 11, + type: 'B' + }, + { + x: '1', + y: 20, + type: 'A' + }, + { + x: '1', + y: 20, + type: 'B' + }, + { + x: 'Subtotal1', + total: true, + collect: 3 + }, + { + x: '2', + y: -60, + type: 'A' + }, + { + x: '2', + y: -40, + type: 'B' + }, + { + x: '3', + y: 20, + type: 'A' + }, + { + x: '3', + y: 20, + type: 'B' + }, + { + x: 'Subtotal2', + total: true, + collect: 3 + }, + { + x: '4', + y: 20, + type: 'A' + }, + { + x: '4', + y: 20, + type: 'B' + }, + { + x: '5', + y: 20, + type: 'A' + }, + { + x: '5', + y: 20, + type: 'B' + }, + { + x: 'Total', + total: true + } + ] + } + ], + series: [ + { + type: 'waterfall', + direction: 'horizontal', + dataIndex: 0, + xField: 'y', + yField: 'x', + seriesField: 'type', + total: { + type: 'field', + tagField: 'total', + startField: 'start', + valueField: 'value', + collectCountField: 'collect' + } + } + ], + axes: [ + { + orient: 'bottom', + type: 'linear', + min: 0 + }, + { + orient: 'left', + label: { + visible: true + }, + type: 'band', + paddingInner: 0.4 + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-leader-line.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-leader-line.mjs new file mode 100644 index 0000000000..8a992defe4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-leader-line.mjs @@ -0,0 +1,130 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6a02d2c286b0c30067ea9ea2 + * 验证目的:反向类目轴瀑布图的连接线与变化值堆叠标签。 + * 保留条件:legends, xField, yField, seriesField, total, stackLabel, title, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'waterfall', + data: { + id: 'id0', + values: [ + { + x: 'First Quarter', + y: 10954, + type: 'Primary Industry' + }, + { + x: 'First Quarter', + y: 106187, + type: 'Secondary Industry' + }, + { + x: 'First Quarter', + y: 153037, + type: 'Tertiary Industry' + }, + { + x: 'Second Quarter', + y: 18183, + type: 'Primary Industry' + }, + { + x: 'Second Quarter', + y: 122450, + type: 'Secondary Industry' + }, + { + x: 'Second Quarter', + y: 151831, + type: 'Tertiary Industry' + }, + { + x: 'Third Quarter', + y: 25642, + type: 'Primary Industry' + }, + { + x: 'Third Quarter', + y: 121553, + type: 'Secondary Industry' + }, + { + x: 'Third Quarter', + y: 160432, + type: 'Tertiary Industry' + }, + { + x: 'Fourth Quarter', + y: 33497, + type: 'Primary Industry' + }, + { + x: 'Fourth Quarter', + y: 132601, + type: 'Secondary Industry' + }, + { + x: 'Fourth Quarter', + y: 169411, + type: 'Tertiary Industry' + }, + { + x: 'Full year', + total: true + } + ] + }, + legends: { + visible: true, + orient: 'bottom' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + total: { + type: 'field', + tagField: 'total' + }, + stackLabel: { + valueType: 'change' + }, + title: { + visible: true, + text: 'Chinese quarterly GDP in 2022' + }, + axes: [ + { + orient: 'left', + title: { + visible: true, + text: 'Unit: 100 million yuan' + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band', + paddingInner: 0.4, + title: { + visible: false + }, + inverse: true + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-summary-total.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-summary-total.mjs new file mode 100644 index 0000000000..335a8ef1af --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-summary-total.mjs @@ -0,0 +1,80 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f2452a1e9eec95f9f1b + * 验证目的:瀑布图自动汇总与标签格式化。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'waterfall', + data: [ + { + id: 'id0', + values: [ + { x: 'Q1-Primary', y: 10954, type: 'Primary' }, + { x: 'Q1-Secondary', y: 106187, type: 'Secondary' }, + { x: 'Q1-Tertiary', y: 153037, type: 'Tertiary' }, + { x: 'Q1', total: true, collect: 3 }, + { x: 'Q2-Primary', y: 18183, type: 'Primary' }, + { x: 'Q2-Secondary', y: 122450, type: 'Secondary' }, + { x: 'Q2-Tertiary', y: 151831, type: 'Tertiary' }, + { x: 'Q2', total: true, collect: 3 }, + { x: 'Q3-Primary', y: 25642, type: 'Primary' }, + { x: 'Q3-Secondary', y: 121553, type: 'Secondary' }, + { x: 'Q3-Tertiary', y: 160432, type: 'Tertiary' }, + { x: 'Q3', total: true, collect: 3 }, + { x: 'Q4-Primary', y: 33497, type: 'Primary' }, + { x: 'Q4-Secondary', y: 132601, type: 'Secondary' }, + { x: 'Q4-Tertiary', y: 169411, type: 'Tertiary' }, + { x: 'Q4', total: true, collect: 3 }, + { x: 'Annual', total: true } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + total: { + type: 'field', + tagField: 'total', + startField: 'start', + valueField: 'value', + collectCountField: 'collect' + }, + stackLabel: { + valueType: 'change' + }, + title: { + visible: true, + text: 'Quarterly GDP in 2022' + }, + legends: { visible: true, orient: 'bottom' }, + axes: [ + { orient: 'left', title: { visible: true, text: 'Unit: 100 million yuan' } }, + { + orient: 'bottom', + label: { + visible: true, + formatMethod: text => { + const arr = text.split('-'); + return arr[arr.length - 1]; + } + }, + type: 'band', + paddingInner: 0.4 + } + ] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-callback.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-callback.mjs new file mode 100644 index 0000000000..861e012c11 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-callback.mjs @@ -0,0 +1,74 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0443 + * 验证目的:瀑布图总计计算回调。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'common', + seriesField: 'type', + data: [ + { + id: 'id0', + values: [ + { x: 'Subtotal', total: true }, + { x: '0', y: 20, type: 'A' }, + { x: '1', y: 20, type: 'A' }, + { x: '2', y: 20, type: 'A' }, + { x: '3', y: 20, type: 'A' }, + { x: '4', y: 20, type: 'A' }, + { x: '5', y: 20, type: 'A' }, + { x: '0', y: 11, type: 'B' }, + { x: '1', y: 20, type: 'B' }, + { x: '2', y: 20, type: 'B' }, + { x: '3', y: 20, type: 'B' }, + { x: '4', y: 20, type: 'B' }, + { x: '5', y: 20, type: 'B' }, + { x: 'Total', total: true } + ] + } + ], + series: [ + { + type: 'waterfall', + dataIndex: 0, + xField: 'x', + yField: 'y', + seriesField: 'type', + total: { + type: 'custom', + tagField: 'total', + product: (datum, current) => { + if (datum.x === 'Subtotal') { + return { + start: 0, + end: 100 + }; + } + return { + start: 0, + end: current.end + }; + } + } + } + ], + axes: [ + { orient: 'left', range: { min: 0 } }, + { orient: 'bottom', label: { visible: true }, type: 'band', paddingInner: 0.4 } + ] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-tag.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-tag.mjs new file mode 100644 index 0000000000..de98f5cfd4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall-total-tag.mjs @@ -0,0 +1,122 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0447 + * 验证目的:瀑布图仅通过标记字段识别总计。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + seriesField: 'type', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + y: 20, + type: 'A' + }, + { + x: '1', + y: 20, + type: 'A' + }, + { + x: '2', + y: 20, + type: 'A' + }, + { + x: '3', + y: 20, + type: 'A' + }, + { + x: '4', + y: 20, + type: 'A' + }, + { + x: '5', + y: 20, + type: 'A' + }, + { + x: '0', + y: 11, + type: 'B' + }, + { + x: '1', + y: 20, + type: 'B' + }, + { + x: '2', + y: 20, + type: 'B' + }, + { + x: '3', + y: 20, + type: 'B' + }, + { + x: '4', + y: 20, + type: 'B' + }, + { + x: '5', + y: 20, + type: 'B' + }, + { + x: 'Total', + total: true + } + ] + } + ], + series: [ + { + type: 'waterfall', + dataIndex: 0, + xField: 'x', + yField: 'y', + seriesField: 'type', + total: { + type: 'field', + tagField: 'total' + } + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band', + paddingInner: 0.4 + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall.mjs b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall.mjs new file mode 100644 index 0000000000..75d2eff29a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/waterfall/waterfall.mjs @@ -0,0 +1,36 @@ +import { verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:累计、总计与连接线(waterfall)。 + * 图表类型:waterfall。 + * 关键配置:total.type、total.tagField、label.visible。 + * 场景条件:固定正负增量和一个由字段标记的总计项。 + * 最终检查:核对累计与总计输入,比较连接线和标签的最终截图。 + * 覆盖边界:不验证横向瀑布图或所有总计模式。 + */ +export default { + createSpec() { + // 总计项使用字段标记,不重复计算累计值。 + return { + type: 'waterfall', + data: { + id: 'data', + values: [ + { x: 'Start', y: 80 }, + { x: 'Growth', y: 25 }, + { x: 'Cost', y: -35 }, + { x: 'Other', y: 10 }, + { x: 'Total', total: true } + ] + }, + xField: 'x', + yField: 'y', + total: { type: 'field', tagField: 'total' }, + label: { visible: true } + }; + }, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/charts/word-cloud/word-cloud-enlarge.mjs b/packages/vchart/__tests__/visual/cases/charts/word-cloud/word-cloud-enlarge.mjs new file mode 100644 index 0000000000..d701e1f5fc --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/charts/word-cloud/word-cloud-enlarge.mjs @@ -0,0 +1,65 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64f751092937835bd5c893f2 + * 验证目的:词云的确定性布局与放大配置。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:fontSizeLimitMax 原配置保留,但不计为最大字号实际限制覆盖;仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'wordCloud', + nameField: 'name', + valueField: 'value', + random: false, + wordCloudConfig: { + zoomToFit: { + enlarge: true, + fontSizeLimitMax: 20 + } + }, + data: { + name: 'baseData', + values: [ + { + name: 'Rice noodles', + value: 957 + }, + { + name: 'Chicken skewers', + value: 942 + }, + { + name: 'Chestnuts', + value: 842 + }, + { + name: 'Pepper soup', + value: 828 + }, + { + name: 'Oden', + value: 665 + }, + { + name: 'Lamb soup', + value: 627 + }, + { + name: 'Sesame noodles', + value: 574 + } + ] + } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-hide.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-hide.mjs new file mode 100644 index 0000000000..79ae2dda88 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-hide.mjs @@ -0,0 +1,78 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65387a631d24511d73c92b74 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:窄图表中三个长时间标签自动隐藏。 + * 保留条件:width=200、sampling=false、greedy、flush、原始时间文本。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'line', + data: { + values: [ + { + time: '2023-10-24 11:11:11', + value: 8 + }, + { + time: '2023-10-25 11:11:11', + value: 9 + }, + { + time: '2023-10-26 11:11:11', + value: 15 + } + ] + }, + axes: [ + { + type: 'band', + orient: 'bottom', + visible: true, + sampling: false, + label: { + visible: true, + space: 4, + style: { + fontSize: 12, + fill: '#6F6F6F', + angle: 0, + fontWeight: 'normal', + direction: 'horizontal' + }, + autoHide: true, + autoHideMethod: 'greedy', + flush: true + }, + hover: true, + background: { + visible: true, + state: { + hover: { + fillOpacity: 0.08, + fill: '#141414' + }, + hover_reverse: { + fillOpacity: 0.08, + fill: '#141414' + } + } + } + } + ], + xField: 'time', + yField: 'value', + width: 200 + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-rotate.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-rotate.mjs new file mode 100644 index 0000000000..b5e6307e9c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-auto-rotate.mjs @@ -0,0 +1,440 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64b7be6214220f89e4720c7f + * 验证目的:分类轴标签自动旋转。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'event_analysis_stack', + values: [ + { + x: '0', + y: 28, + c: 0 + }, + { + x: 0.5, + y: 20, + c: 1 + }, + { + x: 1, + y: 43, + c: 0 + }, + { + x: 1.5, + y: 35, + c: 1 + }, + { + x: 2, + y: 81, + c: 0 + }, + { + x: 2.5, + y: 10, + c: 1 + }, + { + x: 3, + y: 19, + c: 0 + }, + { + x: 3.5, + y: 15, + c: 1 + }, + { + x: 4, + y: 52, + c: 0 + }, + { + x: 4.5, + y: 48, + c: 1 + }, + { + x: 5, + y: 24, + c: 0 + }, + { + x: 5.5, + y: 28, + c: 1 + }, + { + x: 6, + y: 87, + c: 0 + }, + { + x: 6.5, + y: 66, + c: 1 + }, + { + x: 7, + y: 17, + c: 0 + }, + { + x: 7.5, + y: 27, + c: 1 + }, + { + x: 8, + y: 68, + c: 0 + }, + { + x: 8.5, + y: 16, + c: 1 + }, + { + x: 9, + y: 49, + c: 0 + }, + { + x: 9.5, + y: 25, + c: 1 + }, + { + x: 10, + y: 28, + c: 0 + }, + { + x: 10.5, + y: 20, + c: 1 + }, + { + x: 11, + y: 43, + c: 0 + }, + { + x: 11.5, + y: 35, + c: 1 + }, + { + x: 12, + y: 81, + c: 0 + }, + { + x: 12.5, + y: 10, + c: 1 + }, + { + x: 13, + y: 19, + c: 0 + }, + { + x: 13.5, + y: 15, + c: 1 + }, + { + x: 14, + y: 52, + c: 0 + }, + { + x: 14.5, + y: 48, + c: 1 + }, + { + x: 15, + y: 24, + c: 0 + }, + { + x: 15.5, + y: 28, + c: 1 + }, + { + x: 16, + y: 87, + c: 0 + }, + { + x: 16.5, + y: 66, + c: 1 + }, + { + x: 17, + y: 17, + c: 0 + }, + { + x: 17.5, + y: 27, + c: 1 + }, + { + x: 18, + y: 68, + c: 0 + }, + { + x: 18.5, + y: 16, + c: 1 + }, + { + x: 19, + y: 49, + c: 0 + }, + { + x: 19.5, + y: 25, + c: 1 + }, + { + x: 20, + y: 28, + c: 0 + }, + { + x: 20.5, + y: 20, + c: 1 + }, + { + x: 21, + y: 43, + c: 0 + }, + { + x: 21.5, + y: 35, + c: 1 + }, + { + x: 22, + y: 81, + c: 0 + }, + { + x: 22.5, + y: 10, + c: 1 + }, + { + x: 23, + y: 19, + c: 0 + }, + { + x: 23.5, + y: 15, + c: 1 + }, + { + x: 24, + y: 52, + c: 0 + }, + { + x: 24.5, + y: 48, + c: 1 + }, + { + x: 25, + y: 24, + c: 0 + }, + { + x: 25.5, + y: 28, + c: 1 + }, + { + x: 26, + y: 87, + c: 0 + }, + { + x: 26.5, + y: 66, + c: 1 + }, + { + x: 27, + y: 17, + c: 0 + }, + { + x: 27.5, + y: 27, + c: 1 + }, + { + x: 28, + y: 68, + c: 0 + }, + { + x: 28.5, + y: 16, + c: 1 + }, + { + x: 29, + y: 49, + c: 0 + }, + { + x: 29.5, + y: 25, + c: 1 + }, + { + x: 30, + y: 28, + c: 0 + }, + { + x: 30.5, + y: 20, + c: 1 + }, + { + x: 31, + y: 43, + c: 0 + }, + { + x: 31.5, + y: 35, + c: 1 + }, + { + x: 32, + y: 81, + c: 0 + }, + { + x: 32.5, + y: 10, + c: 1 + }, + { + x: 33, + y: 19, + c: 0 + }, + { + x: 33.5, + y: 15, + c: 1 + }, + { + x: 34, + y: 52, + c: 0 + }, + { + x: 34.5, + y: 48, + c: 1 + }, + { + x: 35, + y: 24, + c: 0 + }, + { + x: 35.5, + y: 28, + c: 1 + }, + { + x: 36, + y: 87, + c: 0 + }, + { + x: 36.5, + y: 66, + c: 1 + }, + { + x: 37, + y: 17, + c: 0 + }, + { + x: 37.5, + y: 27, + c: 1 + }, + { + x: 38, + y: 68, + c: 0 + }, + { + x: 38.5, + y: 16, + c: 1 + }, + { + x: 39, + y: 49, + c: 0 + }, + { + x: 39.5, + y: 25, + c: 1 + } + ] + } + ], + xField: 'x', + yField: 'y', + axes: [ + { + orient: 'bottom', + sampling: false, + label: { + autoRotate: true, + autoHide: true + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-break.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-break.mjs new file mode 100644 index 0000000000..dd0a054e67 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-break.mjs @@ -0,0 +1,94 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66d683dc71a15300f7f1fb51 + * 验证目的:数值轴断轴及柱图分段。 + * 保留条件:height, xField, yField, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + height: 400, + type: 'bar', + data: { + values: [ + { + country: 'USA', + visits: 23725 + }, + { + country: 'China', + visits: 1882 + }, + { + country: 'Japan', + visits: 1809 + }, + { + country: 'Germany', + visits: 1322 + }, + { + country: 'UK', + visits: 1122 + }, + { + country: 'France', + visits: 1114 + }, + { + country: 'India', + visits: 984 + }, + { + country: 'Spain', + visits: 711 + }, + { + country: 'Netherlands', + visits: 665 + }, + { + country: 'Russia', + visits: 580 + }, + { + country: 'South Korea', + visits: 443 + }, + { + country: 'Canada', + visits: 441 + } + ] + }, + xField: 'country', + yField: 'visits', + axes: [ + { + orient: 'left', + breaks: [ + { + scopeType: 'count', + range: [2100, 22900] + }, + { + range: [700, 900] + } + ], + domainLine: { + visible: true + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-end-symbol.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-end-symbol.mjs new file mode 100644 index 0000000000..9d39d2d31c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-end-symbol.mjs @@ -0,0 +1,78 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7ee852a1e9eec95f9e73 + * 验证目的:坐标轴端点符号。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + data: [ + { + id: 'line', + values: [ + { + x: 'Monday', + y: 12 + }, + { + x: 'Tuesday', + y: 13 + }, + { + x: 'Wednesday', + y: 11 + }, + { + x: 'Thursday', + y: 10 + }, + { + x: 'Friday', + y: 12 + }, + { + x: 'Saturday', + y: 14 + }, + { + x: 'Sunday', + y: 17 + } + ] + } + ], + xField: 'x', + yField: 'y', + axes: [ + { + zIndex: 100, + orient: 'bottom' + }, + { + zIndex: 100, + orient: 'left', + inverse: true, + domainLine: { + visible: true, + endSymbol: { + visible: true, + style: { + fill: '#000' + } + } + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-explicit-domain.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-explicit-domain.mjs new file mode 100644 index 0000000000..794faf6764 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-explicit-domain.mjs @@ -0,0 +1,200 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64ad3a0c147e41008a658030 + * 验证目的:显式轴定义域与多系列。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + seriesField: 'color', + data: [ + { + id: 'id0', + values: [ + { + x: 'Monday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Monday', + type: 'Lunch', + y: 25 + }, + { + x: 'Tuesday', + type: 'Breakfast', + y: 12 + }, + { + x: 'Tuesday', + type: 'Lunch', + y: 30 + }, + { + x: 'Wednesday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Wednesday', + type: 'Lunch', + y: 24 + }, + { + x: 'Thursday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Thursday', + type: 'Lunch', + y: 25 + }, + { + x: 'Friday', + type: 'Breakfast', + y: 13 + }, + { + x: 'Friday', + type: 'Lunch', + y: 20 + }, + { + x: 'Saturday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Saturday', + type: 'Lunch', + y: 10 + }, + { + x: 'Sunday', + type: 'Breakfast', + y: 20 + }, + { + x: 'Sunday', + type: 'Lunch', + y: 19 + } + ] + }, + { + id: 'id1', + values: [ + { + x: 'Monday', + type: 'Drinks', + y: -52 + }, + { + x: 'Tuesday', + type: 'Drinks', + y: -43 + }, + { + x: 'Wednesday', + type: 'Drinks', + y: -33 + }, + { + x: 'Thursday', + type: 'Drinks', + y: -22 + }, + { + x: 'Friday', + type: 'Drinks', + y: -10 + }, + { + x: 'Saturday', + type: 'Drinks', + y: -30 + }, + { + x: 'Sunday', + type: 'Drinks', + y: -50 + } + ] + } + ], + series: [ + { + type: 'line', + id: 'bar', + dataIndex: 0, + stack: false, + label: { + visible: true + }, + seriesField: 'type', + xField: ['x', 'type'], + yField: 'y' + }, + { + type: 'line', + id: 'line', + dataIndex: 1, + label: { + visible: true + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + stack: false + } + ], + axes: [ + { + orient: 'left', + seriesIndex: [0], + id: 'axisLeft', + nice: false, + zero: false + }, + { + orient: 'right', + seriesId: ['line'], + gird: { + visible: false + }, + nice: false, + zero: false, + sync: { + axisId: 'axisLeft', + zeroAlign: true, + tickAlign: true + }, + label: { + autoLimit: false + }, + maxWidth: '50%' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band', + domain: ['Monday', 'Tuesday', 'Wednesday'] + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-force-tick-count.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-force-tick-count.mjs new file mode 100644 index 0000000000..97280bd017 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-force-tick-count.mjs @@ -0,0 +1,171 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e034d + * 验证目的:多个数值轴的 tickCount 与 forceTickCount 组合。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + axes: [ + { orient: 'left', tick: { tickCount: 5 } }, + { orient: 'left', tick: { tickCount: 1 } }, + { orient: 'left', tick: { tickCount: 0 } }, + { orient: 'left', tick: { forceTickCount: 11 } }, + { orient: 'bottom' } + ], + data: { + values: [ + { x: 'Item 0', type: 'A', y: '897' }, + { x: 'Item 1', type: 'A', y: '799' }, + { x: 'Item 2', type: 'A', y: '636' }, + { x: 'Item 3', type: 'A', y: '752' }, + { x: 'Item 4', type: 'A', y: '874' }, + { x: 'Item 5', type: 'A', y: '881' }, + { x: 'Item 6', type: 'A', y: '755' }, + { x: 'Item 7', type: 'A', y: '684' }, + { x: 'Item 8', type: 'A', y: '758' }, + { x: 'Item 9', type: 'A', y: '620' }, + { x: 'Item 10', type: 'A', y: '698' }, + { x: 'Item 11', type: 'A', y: '785' }, + { x: 'Item 12', type: 'A', y: '633' }, + { x: 'Item 13', type: 'A', y: '734' }, + { x: 'Item 14', type: 'A', y: '880' }, + { x: 'Item 15', type: 'A', y: '723' }, + { x: 'Item 16', type: 'A', y: '865' }, + { x: 'Item 17', type: 'A', y: '785' }, + { x: 'Item 18', type: 'A', y: '706' }, + { x: 'Item 19', type: 'A', y: '771' }, + { x: 'Item 20', type: 'A', y: '697' }, + { x: 'Item 21', type: 'A', y: '733' }, + { x: 'Item 22', type: 'A', y: '740' }, + { x: 'Item 23', type: 'A', y: '602' }, + { x: 'Item 24', type: 'A', y: '646' }, + { x: 'Item 25', type: 'A', y: '700' }, + { x: 'Item 26', type: 'A', y: '679' }, + { x: 'Item 27', type: 'A', y: '627' }, + { x: 'Item 28', type: 'A', y: '756' }, + { x: 'Item 29', type: 'A', y: '669' }, + { x: 'Item 30', type: 'A', y: '783' }, + { x: 'Item 31', type: 'A', y: '883' }, + { x: 'Item 32', type: 'A', y: '701' }, + { x: 'Item 33', type: 'A', y: '657' }, + { x: 'Item 34', type: 'A', y: '816' }, + { x: 'Item 35', type: 'A', y: '815' }, + { x: 'Item 36', type: 'A', y: '617' }, + { x: 'Item 37', type: 'A', y: '654' }, + { x: 'Item 38', type: 'A', y: '763' }, + { x: 'Item 39', type: 'A', y: '716' }, + { x: 'Item 40', type: 'A', y: '601' }, + { x: 'Item 41', type: 'A', y: '776' }, + { x: 'Item 42', type: 'A', y: '897' }, + { x: 'Item 43', type: 'A', y: '706' }, + { x: 'Item 44', type: 'A', y: '864' }, + { x: 'Item 45', type: 'A', y: '874' }, + { x: 'Item 46', type: 'A', y: '605' }, + { x: 'Item 47', type: 'A', y: '879' }, + { x: 'Item 48', type: 'A', y: '703' }, + { x: 'Item 49', type: 'A', y: '827' }, + { x: 'Item 50', type: 'A', y: '883' }, + { x: 'Item 51', type: 'A', y: '818' }, + { x: 'Item 52', type: 'A', y: '685' }, + { x: 'Item 53', type: 'A', y: '806' }, + { x: 'Item 54', type: 'A', y: '631' }, + { x: 'Item 55', type: 'A', y: '846' }, + { x: 'Item 56', type: 'A', y: '887' }, + { x: 'Item 57', type: 'A', y: '652' }, + { x: 'Item 58', type: 'A', y: '855' }, + { x: 'Item 59', type: 'A', y: '619' }, + { x: 'Item 5', type: 'B', y: '701' }, + { x: 'Item 6', type: 'B', y: '685' }, + { x: 'Item 7', type: 'B', y: '756' }, + { x: 'Item 8', type: 'B', y: '799' }, + { x: 'Item 9', type: 'B', y: '628' }, + { x: 'Item 10', type: 'B', y: '852' }, + { x: 'Item 11', type: 'B', y: '689' }, + { x: 'Item 12', type: 'B', y: '827' }, + { x: 'Item 13', type: 'B', y: '665' }, + { x: 'Item 14', type: 'B', y: '725' }, + { x: 'Item 15', type: 'B', y: '694' }, + { x: 'Item 16', type: 'B', y: '741' }, + { x: 'Item 17', type: 'B', y: '876' }, + { x: 'Item 18', type: 'B', y: '750' }, + { x: 'Item 19', type: 'B', y: '839' }, + { x: 'Item 20', type: 'B', y: '825' }, + { x: 'Item 21', type: 'B', y: '657' }, + { x: 'Item 22', type: 'B', y: '607' }, + { x: 'Item 23', type: 'B', y: '795' }, + { x: 'Item 24', type: 'B', y: '770' }, + { x: 'Item 25', type: 'B', y: '686' }, + { x: 'Item 26', type: 'B', y: '741' }, + { x: 'Item 27', type: 'B', y: '623' }, + { x: 'Item 28', type: 'B', y: '644' }, + { x: 'Item 29', type: 'B', y: '862' }, + { x: 'Item 30', type: 'B', y: '737' }, + { x: 'Item 31', type: 'B', y: '802' }, + { x: 'Item 32', type: 'B', y: '708' }, + { x: 'Item 33', type: 'B', y: '672' }, + { x: 'Item 34', type: 'B', y: '743' }, + { x: 'Item 35', type: 'B', y: '718' }, + { x: 'Item 36', type: 'B', y: '635' }, + { x: 'Item 37', type: 'B', y: '797' }, + { x: 'Item 38', type: 'B', y: '685' }, + { x: 'Item 39', type: 'B', y: '638' }, + { x: 'Item 40', type: 'B', y: '840' }, + { x: 'Item 41', type: 'B', y: '797' }, + { x: 'Item 42', type: 'B', y: '852' }, + { x: 'Item 43', type: 'B', y: '752' }, + { x: 'Item 44', type: 'B', y: '760' }, + { x: 'Item 45', type: 'B', y: '862' }, + { x: 'Item 46', type: 'B', y: '611' }, + { x: 'Item 47', type: 'B', y: '694' }, + { x: 'Item 48', type: 'B', y: '740' }, + { x: 'Item 49', type: 'B', y: '856' }, + { x: 'Item 50', type: 'B', y: '776' }, + { x: 'Item 51', type: 'B', y: '856' }, + { x: 'Item 52', type: 'B', y: '759' }, + { x: 'Item 53', type: 'B', y: '768' }, + { x: 'Item 54', type: 'B', y: '883' }, + { x: 'Item 55', type: 'B', y: '626' }, + { x: 'Item 56', type: 'B', y: '746' }, + { x: 'Item 57', type: 'B', y: '614' }, + { x: 'Item 58', type: 'B', y: '717' }, + { x: 'Item 59', type: 'B', y: '654' }, + { x: 'Item 60', type: 'B', y: '613' }, + { x: 'Item 61', type: 'B', y: '757' }, + { x: 'Item 62', type: 'B', y: '606' }, + { x: 'Item 63', type: 'B', y: '755' }, + { x: 'Item 64', type: 'B', y: '699' }, + { x: 'Item 65', type: 'B', y: '761' }, + { x: 'Item 66', type: 'B', y: '630' }, + { x: 'Item 67', type: 'B', y: '814' }, + { x: 'Item 68', type: 'B', y: '856' }, + { x: 'Item 69', type: 'B', y: '630' }, + { x: 'Item 70', type: 'B', y: '833' }, + { x: 'Item 71', type: 'B', y: '809' }, + { x: 'Item 72', type: 'B', y: '702' }, + { x: 'Item 73', type: 'B', y: '822' }, + { x: 'Item 74', type: 'B', y: '878' }, + { x: 'Item 75', type: 'B', y: '774' }, + { x: 'Item 76', type: 'B', y: '652' }, + { x: 'Item 77', type: 'B', y: '704' }, + { x: 'Item 78', type: 'B', y: '691' }, + { x: 'Item 79', type: 'B', y: '616' } + ] + }, + xField: 'x', + yField: 'y', + seriesField: 'type' + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-inside-four-sides.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-inside-four-sides.mjs new file mode 100644 index 0000000000..c3430401f3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-inside-four-sides.mjs @@ -0,0 +1,152 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7eeb52a1e9eec95f9e76 + * 验证目的:四方向轴的内侧刻度、标签格式与轴线组合。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + theme: { fontFamily: 'serif' }, + data: [ + { + id: 'line', + values: [ + { x: 'Monday', y: 12 }, + { x: 'Tuesday', y: 13 }, + { x: 'Wednesday', y: 11 }, + { x: 'Thursday', y: 10 }, + { x: 'Friday', y: 12 }, + { x: 'Saturday', y: 14 }, + { x: 'Sunday', y: 17 } + ] + } + ], + xField: 'x', + yField: 'y', + axes: [ + { + orient: 'right', + title: { visible: true, space: 12, text: '右轴标题' }, + label: { + formatMethod: val => `${val}°C`, + style: { + fill: '#000' + } + }, + tick: { + visible: true, + tickStep: 2, + tickSize: 6, + style: { + stroke: '#000' + } + }, + domainLine: { + visible: true, + style: { + stroke: '#000' + } + }, + grid: { + visible: false + } + }, + { + orient: 'left', + title: { + visible: true, + space: 12, + text: '左轴标题' + }, + label: { + formatMethod: val => `${val}°C`, + style: { + fill: '#000' + } + }, + tick: { + visible: true, + tickStep: 2, + tickSize: 6, + style: { + stroke: '#000' + } + }, + domainLine: { + visible: true, + style: { + stroke: '#000' + } + }, + grid: { + visible: true, + style: { + lineDash: [0] + } + } + }, + { + orient: 'top', + label: { + style: { + fill: '#000' + } + }, + tick: { + inside: true, + tickSize: 8, + style: { + stroke: '#000' + } + }, + domainLine: { + style: { + stroke: '#000' + } + }, + grid: { + visible: true, + style: { + lineDash: [0] + } + } + }, + { + orient: 'bottom', + label: { + inside: true, + style: { + fill: '#000' + } + }, + domainLine: { + style: { + stroke: '#000' + } + }, + grid: { + visible: false + }, + tick: { + inside: true, + tickSize: 8, + style: { + stroke: '#000' + } + } + } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-label-callback.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-label-callback.mjs new file mode 100644 index 0000000000..62321cd382 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-label-callback.mjs @@ -0,0 +1,132 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0358 + * 验证目的:坐标轴标签格式化回调。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'bar', + axes: [ + { + orient: 'bottom', + paddingOuter: 0.1 + }, + { + orient: 'left', + grid: { + visible: false + }, + tick: { + visible: false + }, + domainLine: { + visible: false + }, + tickCount: 3, + label: { + formatMethod: value => { + return `${value}%`; + } + } + } + ], + data: [ + { + name: 'data', + values: [ + { + x: 'Mon', + y: 100, + type: 'Sales' + }, + { + x: 'Tues', + y: 66, + type: 'Sales' + }, + { + x: 'Wed', + y: 95, + type: 'Sales' + }, + { + x: 'Thus', + y: 52, + type: 'Sales' + }, + { + x: 'Fri', + y: 68, + type: 'Sales' + }, + { + x: 'Sat', + y: 52, + type: 'Sales' + }, + { + x: 'sun', + y: 48, + type: 'Sales' + }, + { + x: 'Mon', + y: 43, + type: 'Profit' + }, + { + x: 'Tues', + y: 80, + type: 'Profit' + }, + { + x: 'Wed', + y: 68, + type: 'Profit' + }, + { + x: 'Thus', + y: 40, + type: 'Profit' + }, + { + x: 'Fri', + y: 53, + type: 'Profit' + }, + { + x: 'Sat', + y: 72, + type: 'Profit' + }, + { + x: 'sun', + y: 71, + type: 'Profit' + } + ] + } + ], + xField: ['x', 'type'], + yField: 'y', + seriesField: 'type', + label: { + style: { + visible: false + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-label.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-label.mjs new file mode 100644 index 0000000000..ca0b9f9f6a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-label.mjs @@ -0,0 +1,34 @@ +import { verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:长文本、旋转和轴布局(axis-label-layout)。 + * 图表类型:bar。 + * 关键配置:axes[].label.autoRotate、axes[].label.style.angle、axes[].title。 + * 场景条件:固定长英文标签,显式设置 -35 度,关闭自动旋转。 + * 最终检查:核对轴配置、有效绘制与最终截图。 + * 覆盖边界:不验证自动旋转阈值或全部标签避让分支。 + */ +export default { + createSpec() { + // 显式旋转长标签,避免依赖自动阈值才能触发目标行为。 + return { + type: 'bar', + data: { + id: 'data', + values: ['North America', 'South America', 'Central Europe', 'South East Asia', 'Western Pacific'].map( + (x, i) => ({ x, y: 10 + i * 7 }) + ) + }, + xField: 'x', + yField: 'y', + axes: [ + { orient: 'bottom', label: { autoRotate: false, style: { angle: -35 } } }, + { orient: 'left', title: { visible: true, text: 'Revenue' } } + ] + }; + }, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-log.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-log.mjs new file mode 100644 index 0000000000..1e8d526ff4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-log.mjs @@ -0,0 +1,82 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64c39cd41cfb041e0fa3384c + * 验证目的:对数坐标轴的刻度与折线位置。 + * 保留条件:xField, yField, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: [ + { + values: [ + { + time: 1, + a: 0, + b: 117, + c: 145 + }, + { + time: 10, + a: 1, + b: 1317, + c: 2345 + }, + { + time: 100, + a: 2, + b: 2500, + c: 3100 + }, + { + time: 1000, + a: 3, + b: 7500, + c: 6100 + }, + { + time: 10000, + a: 4, + b: 7500, + c: 6100 + }, + { + time: 100000, + a: 5, + b: 7500, + c: 6100 + }, + { + time: 1000000, + a: 6, + b: 7500, + c: 6100 + } + ] + } + ], + xField: 'time', + yField: 'a', + axes: [ + { + orient: 'left', + type: 'linear' + }, + { + orient: 'bottom', + type: 'log' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-multilevel-wrap.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-multilevel-wrap.mjs new file mode 100644 index 0000000000..eaf5d41878 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-multilevel-wrap.mjs @@ -0,0 +1,126 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 671f2e27c21c5900b3182301 + * 验证目的:多层分类轴标签自动换行。 + * 保留条件:width, xField, yField, seriesField, barGapInGroup, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + width: 200, + data: [ + { + name: 'data1', + values: [ + { + name: 'Product 1', + industry: 'E-commerce', + type: 'Contract amount', + y: 88 + }, + { + name: 'Product 1', + industry: 'E-commerce', + type: 'Paid for', + y: 40 + }, + { + name: 'Product 1', + industry: 'E-commerce', + type: 'Receivables', + y: 78 + }, + { + name: 'Product 1', + industry: 'Game', + type: 'Contract amount', + y: 96 + }, + { + name: 'Product 1', + industry: 'Game', + type: 'Paid for', + y: 70 + }, + { + name: 'Product 1', + industry: 'Game', + type: 'Receivables', + y: 86 + }, + { + name: 'Product 2', + industry: 'E-commerce', + type: 'Contract amount', + y: 96 + }, + { + name: 'Product 2', + industry: 'E-commerce', + type: 'Paid for', + y: 45 + }, + { + name: 'Product 2', + industry: 'E-commerce', + type: 'Receivables', + y: 67 + }, + { + name: 'Product 2', + industry: 'Game', + type: 'Contract amount', + y: 89 + }, + { + name: 'Product 2', + industry: 'Game', + type: 'Paid for', + y: 34 + }, + { + name: 'Product 2', + industry: 'Game', + type: 'Receivables', + y: 50 + } + ] + } + ], + xField: ['name', 'industry', 'type'], + yField: 'y', + seriesField: 'type', + barGapInGroup: 0, + axes: [ + { + orient: 'bottom', + sampling: false, + label: { + autoHide: false, + autoWrap: true, + overlap: false, + style: { + _debug_bounds: true + } + }, + showAllGroupLayers: true, + layers: [ + { + visible: false + } + ] + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-multiple-labels.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-multiple-labels.mjs new file mode 100644 index 0000000000..50c7292c95 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-multiple-labels.mjs @@ -0,0 +1,128 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6593fc588da89e00998d7b76 + * 验证目的:极坐标多层标签布局。 + * 保留条件:categoryField, valueField, seriesField, outerRadius, axes, crosshair;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'rose', + data: { + id: '0', + values: [ + { + time: '2:00', + value: 27, + type: 'Sales' + }, + { + time: '6:00', + value: 25, + type: 'Sales' + }, + { + time: '10:00', + value: 18, + type: 'Sales' + }, + { + time: '14:00', + value: 15, + type: 'Sales' + }, + { + time: '18:00', + value: 10, + type: 'Sales' + }, + { + time: '22:00', + value: 5, + type: 'Sales' + }, + { + time: '2:00', + value: 7, + type: 'Discount' + }, + { + time: '6:00', + value: 5, + type: 'Discount' + }, + { + time: '10:00', + value: 38, + type: 'Discount' + }, + { + time: '14:00', + value: 5, + type: 'Discount' + }, + { + time: '18:00', + value: 20, + type: 'Discount' + }, + { + time: '22:00', + value: 15, + type: 'Discount' + } + ] + }, + categoryField: ['time', 'type'], + valueField: 'value', + seriesField: 'type', + outerRadius: 0.9, + axes: [ + { + orient: 'angle', + showAllGroupLayers: true, + domainLine: { + visible: true + }, + grid: { + visible: true, + alignWithLabel: false + }, + label: { + visible: true + }, + tick: { + visible: true + } + }, + { + orient: 'radius', + grid: { + visible: true, + smooth: true + } + } + ], + crosshair: { + categoryField: { + visible: true, + line: { + type: 'rect' + } + }, + label: { + visible: true + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-title-background.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-title-background.mjs new file mode 100644 index 0000000000..c4ffba0726 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-polar-title-background.mjs @@ -0,0 +1,402 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7ee952a1e9eec95f9e75 + * 验证目的:极坐标轴标题背景与内部刻度。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'rose', + data: [ + { + id: 'barData', + values: [ + { + State: 'AL', + age: 'Under 5 Years', + population: 310504, + type: 'a' + }, + { + State: 'AL', + age: '5 to 13 Years', + population: 552339, + type: 'a' + }, + { + State: 'AL', + age: '14 to 17 Years', + population: 259034, + type: 'a' + }, + { + State: 'AL', + age: '18 to 24 Years', + population: 450818, + type: 'b' + }, + { + State: 'AL', + age: '25 to 44 Years', + population: 1231572, + type: 'c' + }, + { + State: 'AL', + age: '45 to 64 Years', + population: 1215966, + type: 'd' + }, + { + State: 'AL', + age: '65 Years and Over', + population: 641667, + type: 'd' + }, + { + State: 'AK', + age: 'Under 5 Years', + population: 52083, + type: 'a' + }, + { + State: 'AK', + age: '5 to 13 Years', + population: 85640, + type: 'a' + }, + { + State: 'AK', + age: '14 to 17 Years', + population: 42153, + type: 'a' + }, + { + State: 'AK', + age: '18 to 24 Years', + population: 74257, + type: 'b' + }, + { + State: 'AK', + age: '25 to 44 Years', + population: 198724, + type: 'c' + }, + { + State: 'AK', + age: '45 to 64 Years', + population: 183159, + type: 'd' + }, + { + State: 'AK', + age: '65 Years and Over', + population: 50277, + type: 'd' + }, + { + State: 'AZ', + age: 'Under 5 Years', + population: 515910, + type: 'a' + }, + { + State: 'AZ', + age: '5 to 13 Years', + population: 828669, + type: 'a' + }, + { + State: 'AZ', + age: '14 to 17 Years', + population: 362642, + type: 'a' + }, + { + State: 'AZ', + age: '18 to 24 Years', + population: 601943, + type: 'b' + }, + { + State: 'AZ', + age: '25 to 44 Years', + population: 1804762, + type: 'c' + }, + { + State: 'AZ', + age: '45 to 64 Years', + population: 1523681, + type: 'd' + }, + { + State: 'AZ', + age: '65 Years and Over', + population: 862573, + type: 'd' + }, + { + State: 'AR', + age: 'Under 5 Years', + population: 202070, + type: 'a' + }, + { + State: 'AR', + age: '5 to 13 Years', + population: 343207, + type: 'a' + }, + { + State: 'AR', + age: '14 to 17 Years', + population: 157204, + type: 'a' + }, + { + State: 'AR', + age: '18 to 24 Years', + population: 264160, + type: 'b' + }, + { + State: 'AR', + age: '25 to 44 Years', + population: 754420, + type: 'c' + }, + { + State: 'AR', + age: '45 to 64 Years', + population: 727124, + type: 'd' + }, + { + State: 'AR', + age: '65 Years and Over', + population: 407205, + type: 'd' + }, + { + State: 'CA', + age: 'Under 5 Years', + population: 2704659, + type: 'a' + }, + { + State: 'CA', + age: '5 to 13 Years', + population: 4499890, + type: 'a' + }, + { + State: 'CA', + age: '14 to 17 Years', + population: 2159981, + type: 'a' + }, + { + State: 'CA', + age: '18 to 24 Years', + population: 3853788, + type: 'b' + }, + { + State: 'CA', + age: '25 to 44 Years', + population: 10604510, + type: 'c' + }, + { + State: 'CA', + age: '45 to 64 Years', + population: 8819342, + type: 'd' + }, + { + State: 'CA', + age: '65 Years and Over', + population: 4114496, + type: 'd' + }, + { + State: 'CO', + age: 'Under 5 Years', + population: 358280, + type: 'a' + }, + { + State: 'CO', + age: '5 to 13 Years', + population: 587154, + type: 'a' + }, + { + State: 'CO', + age: '14 to 17 Years', + population: 261701, + type: 'a' + }, + { + State: 'CO', + age: '18 to 24 Years', + population: 466194, + type: 'b' + }, + { + State: 'CO', + age: '25 to 44 Years', + population: 1464939, + type: 'c' + }, + { + State: 'CO', + age: '45 to 64 Years', + population: 1290094, + type: 'd' + }, + { + State: 'CO', + age: '65 Years and Over', + population: 511094, + type: 'd' + }, + { + State: 'CT', + age: 'Under 5 Years', + population: 211637, + type: 'a' + }, + { + State: 'CT', + age: '5 to 13 Years', + population: 403658, + type: 'a' + }, + { + State: 'CT', + age: '14 to 17 Years', + population: 196918, + type: 'a' + }, + { + State: 'CT', + age: '18 to 24 Years', + population: 325110, + type: 'b' + }, + { + State: 'CT', + age: '25 to 44 Years', + population: 916955, + type: 'c' + }, + { + State: 'CT', + age: '45 to 64 Years', + population: 968967, + type: 'd' + }, + { + State: 'CT', + age: '65 Years and Over', + population: 478007, + type: 'd' + } + ] + } + ], + padding: { + top: 30 + }, + radius: 1, + innerRadius: 0.6, + categoryField: ['State', 'type'], + valueField: 'population', + seriesField: 'age', + stack: true, + color: ['#98abc5', '#8a89a6', '#7b6888', '#6b486b', '#a05d56', '#d0743c', '#ff8c00'], + axes: [ + { + orient: 'radius', + visible: true, + label: { + visible: true, + formatMethod: val => { + return val / 1000000 + 'M'; + }, + style: { + lineWidth: 2, + stroke: '#fff', + fill: '#000' + } + }, + grid: { + visible: true, + smooth: true, + style: { + lineDash: [0] + } + }, + title: { + visible: true, + position: 'end', + text: 'population', + autoRotate: false, + style: { + textAlign: 'start', + dy: -10, + dx: -10 + } + } + }, + { + orient: 'angle', + visible: true, + inside: true, + title: { + visible: true, + position: 'center', + text: 'State', + padding: 4, + background: { + visible: true, + style: { + fill: 'rgba(0, 0, 0, 0.3)', + cornerRadius: 4 + } + } + }, + tick: { + visible: true, + style: { + stroke: '#000' + } + }, + domainLine: { + visible: true + }, + label: { + visible: true, + style: { + fill: '#000' + } + } + } + ] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-flush.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-flush.mjs new file mode 100644 index 0000000000..31fe1b75ed --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-flush.mjs @@ -0,0 +1,76 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 64b7beba14220f89e4720c80 + * 迁移边界:来源固定 350×200;执行时缩窄到 180×200,形成截断后悬停,保留 flush/autoLimit 配置和原始数据。 + * 验证目的:截断轴标签悬停后显示完整文本(源录制 poptip)。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + height: 200, + width: 350, + data: [ + { + id: 'barData', + values: [ + { month: 'Monday', sales: 22 }, + { month: 'Tuesday', sales: 13 }, + { month: 'Wednesday', sales: 25 }, + { month: 'Thursday', sales: 29 }, + { month: 'Friday', sales: 38 } + ] + } + ], + xField: 'month', + yField: 'sales', + axes: [ + { orient: 'bottom', sampling: false, label: { autoRotate: true, autoLimit: true, autoHide: true } }, + { + orient: 'left', + sampling: false, + label: { autoRotate: true, autoLimit: true, autoHide: true, formatMethod: () => 'AAAAAAAAAAA', flush: true } + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => window.__visualChart.resize(180, 200)); + const p = await page.evaluate(() => { + const g = window.__visualChart + .getStage() + .find( + g => + g.name === 'axis-label' && + g.type === 'text' && + g.cliped && + g.globalAABBBounds.y1 > 30 && + g.globalAABBBounds.y2 < 500, + true + ); + if (!g) throw Error('来源未产生截断轴标签'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2, text: String(g.attribute.text) }; + }); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(p => (window.__poptipTarget = p), p); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.waitForFunction(() => { + const c = window.__visualChart, + g = c.getStage().find(g => g.name === 'poptip', true); + return g && g.attribute.visible !== false && JSON.stringify(g.attribute).includes(window.__poptipTarget?.text); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-rotated.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-rotated.mjs new file mode 100644 index 0000000000..f063b0ed8d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-poptip-rotated.mjs @@ -0,0 +1,119 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 64b7bf3f14220f89e4720c82 + * 迁移边界:定位实际截断的中间轴标签,避免轴交点的遮挡;记录的旧坐标未被当作新宿主的目标。 + * 验证目的:截断轴标签悬停后显示完整文本(源录制 poptip)。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + width: 290, + type: 'line', + axes: [ + { + orient: 'left', + label: { + style: { fontSize: 10, fill: '#000000' }, + autoLimit: true, + formatMethod: val => `+++++++++_${val}_+++++++++` + } + }, + { + orient: 'bottom', + label: { + space: 4, + style: { + fontSize: 12, + fill: '#000000' + }, + autoLimit: true, + autoHide: true, + autoRotate: true, + autoRotateAngle: [0, 30, 45], + formatMethod: val => `++++++_${val}_++++++` + } + } + ], + data: [ + { + id: 'line', + fields: { + y: { + alias: '最高气温' + } + }, + values: [ + { + x: '周一', + y: 12 + }, + { + x: '周二', + y: 13 + }, + { + x: '周三', + y: 11 + }, + { + x: '周四', + y: 10 + }, + { + x: '周五', + y: 12 + }, + { + x: '周六', + y: 14 + }, + { + x: '周日', + y: 17 + } + ] + } + ], + xField: 'x', + yField: 'y' + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await page.evaluate(() => { + const g = window.__visualChart + .getStage() + .find( + g => + g.name === 'axis-label' && + g.type === 'text' && + g.cliped && + g.globalAABBBounds.y1 > 30 && + g.globalAABBBounds.y2 < 500, + true + ); + if (!g) throw Error('来源未产生截断轴标签'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2, text: String(g.attribute.text) }; + }); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(p => (window.__poptipTarget = p), p); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.waitForFunction(() => { + const c = window.__visualChart, + g = c.getStage().find(g => g.name === 'poptip', true); + return g && g.attribute.visible !== false && JSON.stringify(g.attribute).includes(window.__poptipTarget?.text); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-region-overlap.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-region-overlap.mjs new file mode 100644 index 0000000000..a0359c02e9 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-region-overlap.mjs @@ -0,0 +1,103 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65a79c21c82eb000a1d92908 + * 验证目的:关联不同系列的 region-relative-overlap 多轴布局。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'common', + seriesField: 'color', + data: [ + { + id: 'id0', + values: [ + { x: 'Monday', type: 'Breakfast', y: 15 }, + { x: 'Monday', type: 'Lunch', y: 25 }, + { x: 'Tuesday', type: 'Breakfast', y: 12 }, + { x: 'Tuesday', type: 'Lunch', y: 30 }, + { x: 'Wednesday', type: 'Breakfast', y: 15 }, + { x: 'Wednesday', type: 'Lunch', y: 24 }, + { x: 'Thursday', type: 'Breakfast', y: 10 }, + { x: 'Thursday', type: 'Lunch', y: 25 }, + { x: 'Friday', type: 'Breakfast', y: 13 }, + { x: 'Friday', type: 'Lunch', y: 20 }, + { x: 'Saturday', type: 'Breakfast', y: 10 }, + { x: 'Saturday', type: 'Lunch', y: 22 }, + { x: 'Sunday', type: 'Breakfast', y: 12 }, + { x: 'Sunday', type: 'Lunch', y: 19 } + ] + }, + { + id: 'id1', + values: [ + { x: 'Monday', type: 'Drinks', y: 22 }, + { x: 'Tuesday', type: 'Drinks', y: 43 }, + { x: 'Wednesday', type: 'Drinks', y: 33 }, + { x: 'Thursday', type: 'Drinks', y: 22 }, + { x: 'Friday', type: 'Drinks', y: 10 }, + { x: 'Saturday', type: 'Drinks', y: 30 }, + { x: 'Sunday', type: 'Drinks', y: 50 } + ] + } + ], + series: [ + { + type: 'bar', + id: 'bar', + dataIndex: 0, + label: { visible: true }, + seriesField: 'type', + xField: ['x', 'type'], + yField: 'y' + }, + { + type: 'line', + id: 'line', + dataIndex: 1, + label: { visible: true }, + seriesField: 'type', + xField: 'x', + yField: 'y', + stack: false + } + ], + axes: [ + { orient: 'left', label: { style: { fill: 'red' } } }, + { orient: 'left', layoutType: 'region-relative-overlap', seriesIndex: [0], label: { style: { fill: 'red' } } }, + { + orient: 'left', + seriesId: ['line'], + gird: { visible: false }, + layoutType: 'region-relative-overlap', + label: { + style: { dy: -14, fill: 'blue' }, + formatMethod: () => { + return 12345678; + } + } + }, + { + orient: 'left', + seriesId: ['line'], + gird: { visible: false }, + layoutType: 'region-relative-overlap', + label: { style: { dy: 14, fill: 'green' } } + }, + { orient: 'left', seriesId: ['line'], gird: { visible: false }, label: { style: { dy: 14, fill: 'green' } } }, + { orient: 'bottom', label: { visible: true }, type: 'band' } + ], + legends: { visible: true, orient: 'top', position: 'start' } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-symlog.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-symlog.mjs new file mode 100644 index 0000000000..741671f752 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-symlog.mjs @@ -0,0 +1,153 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64e85a47707420a88d0e692e + * 验证目的:相同正负数据在线性轴和对称对数轴的布局。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const symexp = c => { + return x => { + return Math.sign(x) * Math.expm1(Math.abs(x)) * c; + }; + }; + const scale = symexp(10); + + const data = []; + for (let i = -5; i < 6; i++) { + data.push({ + x: scale(i), + y: i + }); + } + const spec = { + type: 'common', + layout: { + type: 'grid', + col: 2, + row: 6, + rowHeight: [ + { + index: 0, + size: 30 + }, + { + index: 3, + size: 20 + } + ], + elements: [ + { + modelId: 'title', + col: 1, + row: 0 + }, + { + modelId: 'line-region-A', + col: 1, + row: 1 + }, + { + modelId: 'axis-left-A', + col: 0, + row: 1 + }, + { + modelId: 'axis-bottom-A', + col: 1, + row: 2 + }, + { + modelId: 'line-region-B', + col: 1, + row: 4 + }, + { + modelId: 'axis-left-B', + col: 0, + row: 4 + }, + { + modelId: 'axis-bottom-B', + col: 1, + row: 5 + } + ] + }, + region: [ + { + id: 'line-region-A' + }, + { + id: 'line-region-B' + } + ], + series: [ + { + regionId: 'line-region-A', + type: 'line', + xField: 'x', + yField: 'y', + data: { + id: 'line-A', + values: data + } + }, + { + regionId: 'line-region-B', + type: 'line', + xField: 'x', + yField: 'y', + data: { + id: 'line-B', + values: data + } + } + ], + title: { + text: 'the example shows difference of linear axis and symlog axis', + id: 'title' + }, + axes: [ + { + id: 'axis-left-A', + regionId: 'line-region-A', + orient: 'left', + type: 'linear' + }, + + { + id: 'axis-bottom-A', + regionId: 'line-region-A', + orient: 'bottom', + type: 'linear', + title: 'log-axis' + }, + { + id: 'axis-left-B', + regionId: 'line-region-B', + orient: 'left', + type: 'linear' + }, + + { + id: 'axis-bottom-B', + regionId: 'line-region-B', + orient: 'bottom', + type: 'symlog', + title: 'log-axis' + } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-sync-scrollbar.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-sync-scrollbar.mjs new file mode 100644 index 0000000000..d61bf612ce --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-sync-scrollbar.mjs @@ -0,0 +1,173 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 665323210b3b8900c63a78b0 + * 验证目的:横向柱图的双数值轴刻度同步与滚动条。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'common', + padding: { left: 0, right: 0, bottom: 0, top: 0 }, + legends: [{ type: 'discrete', visible: true, orient: 'bottom', maxRow: 1, item: { label: {} } }], + data: [ + { + id: 'seriesData', + values: [ + { tagShowValue: '[87.00,130.50)', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[43.50,87.00)', segName: 'ccc', percentage: 2.78, tgi: 778 }, + { tagShowValue: '[391.50,435.00]', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[348.00,391.50)', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[304.50,348.00)', segName: 'ccc', percentage: 2.78, tgi: 778 }, + { tagShowValue: '[261.00,304.50)', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[217.50,261.00)', segName: 'ccc', percentage: 2.78, tgi: 778 }, + { tagShowValue: '[174.00,217.50)', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[130.50,174.00)', segName: 'ccc', percentage: 2.78, tgi: 778 }, + { tagShowValue: '[0.00,43.50)', segName: 'ccc', percentage: 2.85, tgi: 778 }, + { tagShowValue: '[87.00,130.50)', segName: 'fasfdsafdsa', percentage: 0.75, tgi: 205 }, + { tagShowValue: '[43.50,87.00)', segName: 'fasfdsafdsa', percentage: 0.74, tgi: 205 }, + { tagShowValue: '[391.50,435.00]', segName: 'fasfdsafdsa', percentage: 0.75, tgi: 205 }, + { tagShowValue: '[348.00,391.50)', segName: 'fasfdsafdsa', percentage: 0.75, tgi: 205 }, + { tagShowValue: '[304.50,348.00)', segName: 'fasfdsafdsa', percentage: 0.74, tgi: 205 }, + { tagShowValue: '[261.00,304.50)', segName: 'fasfdsafdsa', percentage: 0.75, tgi: 205 }, + { tagShowValue: '[217.50,261.00)', segName: 'fasfdsafdsa', percentage: 0.74, tgi: 205 }, + { tagShowValue: '[174.00,217.50)', segName: 'fasfdsafdsa', percentage: 0.75, tgi: 205 }, + { tagShowValue: '[130.50,174.00)', segName: 'fasfdsafdsa', percentage: 0.74, tgi: 205 }, + { tagShowValue: '[0.00,43.50)', segName: 'fasfdsafdsa', percentage: 0.74, tgi: 201 }, + { tagShowValue: '[87.00,130.50)', segName: '分群2', percentage: 0.25, tgi: 0 }, + { tagShowValue: '[43.50,87.00)', segName: '分群2', percentage: 0.33, tgi: 0 }, + { tagShowValue: '[391.50,435.00]', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[348.00,391.50)', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[304.50,348.00)', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[261.00,304.50)', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[217.50,261.00)', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[174.00,217.50)', segName: '分群2', percentage: 0, tgi: 0 }, + { tagShowValue: '[130.50,174.00)', segName: '分群2', percentage: 0.99, tgi: 0 }, + { tagShowValue: '[0.00,43.50)', segName: '分群2', percentage: 0.23, tgi: 0 }, + { tagShowValue: '[87.00,130.50)', segName: '分群1', percentage: 0.87, tgi: 0 }, + { tagShowValue: '[43.50,87.00)', segName: '分群1', percentage: 0.27, tgi: 0 }, + { tagShowValue: '[391.50,435.00]', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[348.00,391.50)', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[304.50,348.00)', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[261.00,304.50)', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[217.50,261.00)', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[174.00,217.50)', segName: '分群1', percentage: 0, tgi: 0 }, + { tagShowValue: '[130.50,174.00)', segName: '分群1', percentage: 0.1, tgi: 0 }, + { tagShowValue: '[0.00,43.50)', segName: '分群1', percentage: 0.77, tgi: 0 } + ] + } + ], + autoBandSize: true, + series: [ + { + type: 'bar', + id: 'barSeries', + dataId: 'seriesData', + direction: 'horizontal', + yField: ['tagShowValue', 'segName'], + xField: 'percentage', + seriesField: 'segName', + stack: false, + barWidth: 20, + bar: { style: { lineWidth: 0 } } + }, + { + type: 'bar', + id: 'tgiBarSeries', + dataId: 'seriesData', + direction: 'horizontal', + yField: ['tagShowValue', 'segName'], + xField: 'tgi', + barWidth: 20, + bar: { style: { fill: { field: 'segName', scale: 'tgiColorScale' } } } + } + ], + axes: [ + { + orient: 'bottom', + id: 'leftId', + type: 'linear', + seriesId: ['barSeries'], + label: { padding: 6 }, + gridDash: [0, 0], + title: { visible: false }, + unit: { visible: true, margin: { left: -20 } } + }, + { + visible: true, + orient: 'top', + type: 'linear', + sync: { axisId: 'leftId', tickAlign: true }, + seriesId: ['tgiBarSeries'], + label: { padding: 6 }, + gridDash: [0, 0], + zero: true, + label: { formatMethod: label => label.toFixed(2) } + }, + { orient: 'left', type: 'band' } + ], + markLine: [ + { + x: 100, + relativeSeriesId: 'tgiBarSeries', + label: { + visible: true, + confine: true, + text: 'TGI 100', + style: { fontSize: 12, fontWeight: 'bold', fill: 'red', dx: 10, suffixPosition: 'end' }, + labelBackground: { visible: false } + }, + startSymbol: { visible: false }, + endSymbol: { visible: false }, + line: { label: { visible: true, text: 'TGI 100' }, style: { stroke: 'red', lineDash: [4] } } + } + ], + scales: [ + { + id: 'colorScale', + type: 'ordinal', + domain: [{ dataId: 'seriesData', fields: ['segName'] }], + range: [ + '#1664FF', + '#3CC780', + '#FF8A00', + '#1AC6FF', + '#7442D4', + '#FFC400', + '#B48DEB', + '#009488', + '#304D77', + '#FF7DDA' + ] + }, + { + id: 'tgiColorScale', + type: 'ordinal', + domain: [{ dataId: 'seriesData', fields: ['segName'] }], + range: [ + '#B2CFFF', + 'rgb(185, 237, 205)', + '#FFCE7A', + '#94EFFF', + '#DDC5FA', + '#FAE878', + '#EFE3FF', + '#59BAA8', + '#8B959E', + '#FFCFEE' + ] + } + ], + scrollBar: [{ orient: 'right', auto: true }] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-align.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-align.mjs new file mode 100644 index 0000000000..17c2de24c2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-align.mjs @@ -0,0 +1,200 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64ab81a9a7d9c63765bac951 + * 验证目的:双数值轴的刻度对齐。 + * 保留条件:seriesField, series, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + seriesField: 'color', + data: [ + { + id: 'id0', + values: [ + { + x: 'Monday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Monday', + type: 'Lunch', + y: 25 + }, + { + x: 'Tuesday', + type: 'Breakfast', + y: 12 + }, + { + x: 'Tuesday', + type: 'Lunch', + y: 30 + }, + { + x: 'Wednesday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Wednesday', + type: 'Lunch', + y: 24 + }, + { + x: 'Thursday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Thursday', + type: 'Lunch', + y: 25 + }, + { + x: 'Friday', + type: 'Breakfast', + y: 13 + }, + { + x: 'Friday', + type: 'Lunch', + y: 20 + }, + { + x: 'Saturday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Saturday', + type: 'Lunch', + y: 10 + }, + { + x: 'Sunday', + type: 'Breakfast', + y: 20 + }, + { + x: 'Sunday', + type: 'Lunch', + y: 19 + } + ] + }, + { + id: 'id1', + values: [ + { + x: 'Monday', + type: 'Drinks', + y: -52 + }, + { + x: 'Tuesday', + type: 'Drinks', + y: -43 + }, + { + x: 'Wednesday', + type: 'Drinks', + y: -33 + }, + { + x: 'Thursday', + type: 'Drinks', + y: -22 + }, + { + x: 'Friday', + type: 'Drinks', + y: -10 + }, + { + x: 'Saturday', + type: 'Drinks', + y: -30 + }, + { + x: 'Sunday', + type: 'Drinks', + y: -50 + } + ] + } + ], + series: [ + { + type: 'line', + id: 'bar', + dataIndex: 0, + stack: false, + label: { + visible: true + }, + seriesField: 'type', + xField: ['x', 'type'], + yField: 'y' + }, + { + type: 'line', + id: 'line', + dataIndex: 1, + label: { + visible: true + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + stack: false + } + ], + axes: [ + { + orient: 'left', + seriesIndex: [0], + id: 'axisLeft', + nice: false, + zero: false + }, + { + orient: 'right', + seriesId: ['line'], + gird: { + visible: false + }, + label: { + autoLimit: false + }, + maxWidth: '50%', + nice: false, + zero: false, + sync: { + axisId: 'axisLeft', + zeroAlign: true, + tickAlign: true + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-count-callback.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-count-callback.mjs new file mode 100644 index 0000000000..60e023d33a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-tick-count-callback.mjs @@ -0,0 +1,57 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 6509407dd99606ff3a6fec86 + * 验证目的:依据轴长和字体计算数值轴刻度数量。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'bar', + height: 200, + data: [ + { + name: 'bar', + fields: { y: { alias: 'Quantity' } }, + values: [ + { x: '2021-12-21 2:00', y: 82 }, + { x: '2021-12-21 4:00', y: 50 }, + { x: '2021-12-21 6:00', y: 64 }, + { x: '2021-12-21 8:00', y: 30 }, + { x: '2021-12-21 10:00', y: 40 }, + { x: '2021-12-21 12:00', y: 40 }, + { x: '2021-12-21 14:00', y: 56 }, + { x: '2021-12-21 16:00', y: 40 }, + { x: '2021-12-21 18:00', y: 64 }, + { x: '2021-12-21 20:00', y: 74 }, + { x: '2021-12-21 22:00', y: 98 } + ] + } + ], + xField: 'x', + yField: 'y', + axes: [ + { + orient: 'left', + tick: { + tickCount: ({ axisLength, labelStyle }) => { + const density = 1; + const height = axisLength ?? 200; + const fontSize = labelStyle?.fontSize ?? 12; + return Math.max(Math.ceil((height / (fontSize * 1.5)) * (0.2 * density)), 2); + } + } + } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-time-brush.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-time-brush.mjs new file mode 100644 index 0000000000..3f64fb489d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-time-brush.mjs @@ -0,0 +1,118 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66d52aca42109e00f2e0713e + * 验证目的:时间轴和 dataZoom 及刷选配置共存。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + + const spec = { + type: 'line', + data: { + values: [ + { time: 1720339200000, value: 7, legend: 'Excellent' }, + { time: 1720339800000, value: 37, legend: 'Excellent' }, + { time: 1720340400000, value: 10, legend: 'Excellent' }, + { time: 1720341000000, value: 37, legend: 'Excellent' }, + { time: 1720341600000, value: 37, legend: 'Excellent' }, + { time: 1720342200000, value: 37, legend: 'Excellent' }, + { time: 1720342800000, value: 3, legend: 'Excellent' }, + { time: 1720343400000, value: 37, legend: 'Excellent' }, + { time: 1720344000000, value: 3, legend: 'Excellent' }, + { time: 1720345800000, value: 3, legend: 'Excellent' }, + { time: 1720346400000, value: 37, legend: 'Excellent' }, + { time: 1720347000000, value: 37, legend: 'Excellent' }, + { time: 1720347600000, value: 3, legend: 'Excellent' }, + { time: 1720348200000, value: 37, legend: 'Excellent' } + ] + }, + xField: 'time', + yField: 'value', + axes: [ + { + orient: 'bottom', + type: 'time', + layers: [ + { + // 格式化x时间轴 + timeFormat: '%m/%d %H:%M' + } + ] + // min:1720334200000, + // max:1720349800000 + } + ], + brush: { + visible: true, + brushType: 'x', + // 开启后默认关联所有axis/dataZoom + zoomAfterBrush: true, + zoomWhenEmpty: false, + brushMoved: false, // 选框是否可被平移 + delayType: 'throttle', + style: { + shadowColor: '#1664FF1A', + lineWidth: 0.5 + } + }, + dataZoom: [ + { + orient: 'bottom', + showDetail: false, + visible: true + } + ], + crosshair: { + xField: { + visible: true, + line: { + type: 'line' + } + } + }, + point: { + style: { + size: 4 + }, + state: { + dimension_hover: { + size: 10 + } + } + }, + tooltip: { + dimension: { + visible: (a, b, c) => { + return a[0].value > 1720339200000 && a[0].value < 1720348200000; + }, + title: { + valueTimeFormat: '%Y-%m-%d %H:%M:%S' + }, + content: { + key: datum => datum.legend, + value: datum => { + return datum.value; + } + } + } + }, + line: { + style: { + lineWidth: 2 + } + }, + legends: { visible: true } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-time-layer-step.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-time-layer-step.mjs new file mode 100644 index 0000000000..1ef271e75e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-time-layer-step.mjs @@ -0,0 +1,49 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64afb5dc2a6e789306c890fa + * 验证目的:时间轴 layers 的 tickStep 与混合范围数据。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'rangeColumn', + data: [ + { + id: 'data0', + values: [ + { type: 'Category A', min: 1224836868000, max: 1224923268000 }, + { type: 'Category B', min: 1225009668000, max: 1225182468000 }, + { type: 'Category C', min: 1225182468000, max: 1225268868000 }, + { type: 'Category D', min: 58, max: 155 }, + { type: 'Category E', min: 45, max: 120 }, + { type: 'Category F', min: 23, max: 99 }, + { type: 'Category G', min: 18, max: 56 }, + { type: 'Category H', min: 18, max: 34 } + ] + } + ], + direction: 'horizontal', + yField: 'type', + xField: ['min', 'max'], + label: { visible: false, position: 'bothEnd' }, + axes: [ + { + orient: 'bottom', + type: 'time', + range: { min: 1224836868000, max: 1240475268000 }, + layers: [{ tickStep: 28800, timeFormat: '%Y%m%d' }] + } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-trim-padding.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-trim-padding.mjs new file mode 100644 index 0000000000..cb40bf5b6c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-trim-padding.mjs @@ -0,0 +1,237 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 655da4a5e5f1d64646b72955 + * 验证目的:面积图分类轴两端留白裁剪。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + stack: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + axes: [ + { + orient: 'bottom', + trimPadding: true, + label: { + flush: true + } + }, + { + orient: 'left', + label: { + flush: true + } + } + ], + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + data: { + fields: { + country: { + domain: ['China', 'USA', 'EU', 'Africa'], + sortIndex: 0 + } + }, + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-offset.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-offset.mjs new file mode 100644 index 0000000000..dda9e7d714 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-offset.mjs @@ -0,0 +1,147 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 652f89517b1b18b9934f7e17 + * 验证目的:自动缩进与单位文本偏移。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + Age: 'Under 5 Years', + Population: 25635 + }, + { + State: 'WY', + Age: '5 to 13 Years', + Population: 1890 + }, + { + State: 'WY', + Age: '14 to 17 Years', + Population: 9314 + }, + { + State: 'DC', + Age: 'Under 5 Years', + Population: 30352 + }, + { + State: 'DC', + Age: '5 to 13 Years', + Population: 20439 + }, + { + State: 'DC', + Age: '14 to 17 Years', + Population: 10225 + }, + { + State: 'VT', + Age: 'Under 5 Years', + Population: 38253 + }, + { + State: 'VT', + Age: '5 to 13 Years', + Population: 42538 + }, + { + State: 'VT', + Age: '14 to 17 Years', + Population: 15757 + }, + { + State: 'ND', + Age: 'Under 5 Years', + Population: 51896 + }, + { + State: 'ND', + Age: '5 to 13 Years', + Population: 67358 + }, + { + State: 'ND', + Age: '14 to 17 Years', + Population: 18794 + }, + { + State: 'AK', + Age: 'Under 5 Years', + Population: 72083 + }, + { + State: 'AK', + Age: '5 to 13 Years', + Population: 85640 + }, + { + State: 'AK', + Age: '14 to 17 Years', + Population: 22153 + } + ] + } + ], + xField: 'State', + yField: 'Population', + seriesField: 'Age', + stack: true, + legends: { + visible: true + }, + bar: { + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + }, + axes: [ + { + orient: 'left', + autoIndent: true, + label: { + style: {} + }, + unit: { + visible: true, + text: 'Celsius', + style: { + dy: -10 + } + } + }, + { + orient: 'bottom', + inverse: true, + unit: { + visible: true, + text: 'Celsius', + style: { + textAlign: 'right', + textBaseline: 'top', + dy: 10 + } + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-position.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-position.mjs new file mode 100644 index 0000000000..6324014ccb --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-unit-position.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 660bbdfa5a32c100a2a88ae0 + * 验证目的:轴单位在指定位置显示。 + * 保留条件:xField, yField, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value', + axes: [ + { + orient: 'left', + min: 3, + max: 29, + unit: { + visible: true, + text: 'Title' + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-vertical-text.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-vertical-text.mjs new file mode 100644 index 0000000000..e481a49002 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-vertical-text.mjs @@ -0,0 +1,226 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 650c3006f88ab33465ca32ce + * 验证目的:坐标轴标签竖排文字。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: { + visible: true + }, + axes: [ + { + orient: 'bottom', + type: 'band', + label: { + style: { + direction: 'vertical' + } + } + }, + { + orient: 'left', + type: 'linear' + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-visible-layers.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-visible-layers.mjs new file mode 100644 index 0000000000..c6548a6fcb --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-visible-layers.mjs @@ -0,0 +1,122 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65b8ca75c00fc9008c750045 + * 验证目的:分类轴多层标签显示。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + values: [ + { + type: 'Category One', + min: 76, + max: 100, + range: 'A', + type2: 'p', + color: 'A_p' + }, + { + type: 'Category Two', + min: 56, + max: 108, + range: 'A', + type2: 'p', + color: 'A_p' + }, + { + type: 'Category One', + min: 56, + max: 100, + range: 'B', + type2: 'p', + color: 'B_p' + }, + { + type: 'Category Two', + min: 36, + max: 108, + range: 'B', + type2: 'p', + color: 'B_p' + }, + { + type: 'Category One', + min: 76, + max: 100, + range: 'A', + type2: 'k', + color: 'A_k' + }, + { + type: 'Category Two', + min: 56, + max: 108, + range: 'A', + type2: 'k', + color: 'A_k' + }, + { + type: 'Category One', + min: 56, + max: 100, + range: 'B', + type2: 'k', + color: 'B_k' + }, + { + type: 'Category Two', + min: 36, + max: 108, + range: 'B', + type2: 'k', + color: 'B_k' + } + ] + } + ], + xField: ['type', 'range', 'type2'], + yField: 'min', + seriesField: 'color', + barWidth: 20, + paddingInner: [0.6, 0.6, 0.6], + bandPadding: [0.6, 0.6, 0.6], + label: { + position: 'bothEnd' + }, + axes: [ + { + orient: 'bottom', + showAllGroupLayers: true, + layers: [ + { + visible: true + }, + { + visible: false + }, + {} + ], + label: {}, + tick: { + tickCount: 2 + } + } + ], + legends: { + visible: true + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/axis/axis-zero-align.mjs b/packages/vchart/__tests__/visual/cases/components/axis/axis-zero-align.mjs new file mode 100644 index 0000000000..86cdc65b6c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/axis/axis-zero-align.mjs @@ -0,0 +1,195 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64ab8167bcd54d969f2208d5 + * 验证目的:双数值轴在零点两侧对齐。 + * 保留条件:seriesField, series, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + seriesField: 'color', + data: [ + { + id: 'id0', + values: [ + { + x: 'Monday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Monday', + type: 'Lunch', + y: 25 + }, + { + x: 'Tuesday', + type: 'Breakfast', + y: 12 + }, + { + x: 'Tuesday', + type: 'Lunch', + y: 30 + }, + { + x: 'Wednesday', + type: 'Breakfast', + y: 15 + }, + { + x: 'Wednesday', + type: 'Lunch', + y: 24 + }, + { + x: 'Thursday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Thursday', + type: 'Lunch', + y: 25 + }, + { + x: 'Friday', + type: 'Breakfast', + y: 13 + }, + { + x: 'Friday', + type: 'Lunch', + y: 20 + }, + { + x: 'Saturday', + type: 'Breakfast', + y: 10 + }, + { + x: 'Saturday', + type: 'Lunch', + y: 10 + }, + { + x: 'Sunday', + type: 'Breakfast', + y: 20 + }, + { + x: 'Sunday', + type: 'Lunch', + y: 19 + } + ] + }, + { + id: 'id1', + values: [ + { + x: 'Monday', + type: 'Drinks', + y: -52 + }, + { + x: 'Tuesday', + type: 'Drinks', + y: -43 + }, + { + x: 'Wednesday', + type: 'Drinks', + y: -33 + }, + { + x: 'Thursday', + type: 'Drinks', + y: -22 + }, + { + x: 'Friday', + type: 'Drinks', + y: -10 + }, + { + x: 'Saturday', + type: 'Drinks', + y: -30 + }, + { + x: 'Sunday', + type: 'Drinks', + y: -50 + } + ] + } + ], + series: [ + { + type: 'line', + id: 'bar', + dataIndex: 0, + stack: false, + label: { + visible: true + }, + seriesField: 'type', + xField: ['x', 'type'], + yField: 'y' + }, + { + type: 'line', + id: 'line', + dataIndex: 1, + label: { + visible: true + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + stack: false + } + ], + axes: [ + { + orient: 'left', + seriesIndex: [0], + id: 'axisLeft', + nice: false, + zero: false + }, + { + orient: 'right', + seriesId: ['line'], + gird: { + visible: false + }, + nice: false, + zero: false, + sync: { + axisId: 'axisLeft', + zeroAlign: true + } + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/brush/brush-select.mjs b/packages/vchart/__tests__/visual/cases/components/brush/brush-select.mjs new file mode 100644 index 0000000000..06f821c11b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/brush/brush-select.mjs @@ -0,0 +1,127 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64d05b3ea77f904af107fe73 + * 验证目的:拖拽矩形刷选后区分命中和未命中的散点。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:保留来源动作或按源事件执行操作并检查结果。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'scatter', + data: [ + { + values: [ + { x: 936196, size: 83431, y: 1371, type: 'Technology', area: 'Northeast' }, + { x: 1270911, size: 219815, y: 5590, type: 'Office supplies', area: 'Central South' }, + { x: 453898, size: 19061, y: 727, type: 'Technology', area: 'Southwest' }, + { x: 919743, size: 148800, y: 1199, type: 'Furniture', area: 'North' }, + { x: 1676224, size: 163453, y: 2517, type: 'Furniture', area: 'East' }, + { x: 1466575, size: 251487, y: 2087, type: 'Technology', area: 'Central South' }, + { x: 824673, size: 86067, y: 3622, type: 'Office supplies', area: 'Northeast' }, + { x: 230956, size: 24016, y: 347, type: 'Technology', area: 'Northwest' }, + { x: 1599653, size: 228179, y: 2183, type: 'Technology', area: 'East' }, + { x: 745813, size: 137265, y: 3020, type: 'Office supplies', area: 'North' }, + { x: 267870, size: 49633, y: 970, type: 'Office supplies', area: 'Northwest' }, + { x: 1408628, size: 215585, y: 6341, type: 'Office supplies', area: 'East' }, + { x: 781743, size: 144986, y: 927, type: 'Technology', area: 'North' }, + { x: 501533, size: 29303, y: 814, type: 'Furniture', area: 'Southwest' }, + { x: 920698, size: 72692, y: 1470, type: 'Furniture', area: 'Northeast' }, + { x: 316212, size: 24903, y: 468, type: 'Furniture', area: 'Northwest' }, + { x: 1399928, size: 199582, y: 2023, type: 'Furniture', area: 'Central South' }, + { x: 347692, size: 49272, y: 1858, type: 'Office supplies', area: 'Southwest' } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + sizeField: 'size', + size: [10, 25], + shapeField: 'type', + shape: ['circle', 'triangle'], + axes: [ + { orient: 'left', range: { min: 0 }, type: 'linear' }, + { orient: 'bottom', label: { visible: true }, type: 'linear' } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { + visible: true, + style: { + text: 'Title' + } + }, + item: { + visible: true + } + } + ], + direction: 'horizontal', + brush: { + brushType: 'rect', + inBrush: { + colorAlpha: 1 + }, + outOfBrush: { + colorAlpha: 0.2 + }, + sizeThreshold: 8 + }, + tooltip: { + transitionDuration: 0 + } + }; + + return spec; + }, + async exercise(page) { + // 两次刷选之间执行源录制中的空白点击,保留创建、清除、再次创建的真实结果。 + const records = []; + await page.evaluate(() => { + window.__brushClears = 0; + window.__visualChart.on('brushClear', () => { + window.__brushClears++; + }); + }); + for (const index of [0, 1]) { + const p = await interactionTarget(page, 'point', index); + await page.mouse.move(p.x - 18, p.y - 18); + await page.mouse.down(); + await page.mouse.move(p.x + 18, p.y + 18, { steps: 8 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + records.push(await seriesStates(page)); + if (index === 0) { + await page.mouse.click(790, 590); + await interactionFrame(page); + records.push(await seriesStates(page)); + } + } + await page.evaluate(records => { + window.__brushObservations = records; + }, records); + }, + async verify(page) { + // 既检查两次非空刷选,又要求中间点击清空所有刷选状态。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const rows = window.__brushObservations; + if (rows?.length !== 3 || window.__brushClears < 1) throw Error('缺少刷选清除过程'); + for (const i of [0, 2]) + if (!rows[i].some(m => m.states.inBrush > 0) || !rows[i].some(m => m.states.outOfBrush > 0)) + throw Error('刷选未区分命中与未命中'); + if (rows[1].some(m => m.states.inBrush > 0 || m.states.outOfBrush > 0)) throw Error('空白点击没有清除刷选状态'); + const g = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + if (!g.some(g => g.currentStates?.includes('inBrush'))) throw Error('最终刷选缺失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-dual-axis-default.mjs b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-dual-axis-default.mjs new file mode 100644 index 0000000000..fc3338e6fe --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-dual-axis-default.mjs @@ -0,0 +1,191 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7ef952a1e9eec95f9e9e + * 验证目的:双轴绑定的默认选中十字线。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: [ + { + id: 'line', + values: [ + { + x: 'Round 1', + y: 21, + c: 'Green slime' + }, + { + x: 'Round 1', + y: 38, + c: 'Flying pig' + }, + { + x: 'Round 2', + y: 28, + c: 'Green slime' + }, + { + x: 'Round 2', + y: 45, + c: 'Flying pig' + }, + { + x: 'Round 3', + y: 22, + c: 'Green slime' + }, + { + x: 'Round 3', + y: 56, + c: 'Flying pig' + }, + { + x: 'Round 4', + y: 34, + c: 'Green slime' + }, + { + x: 'Round 4', + y: 48, + c: 'Flying pig' + }, + { + x: 'Round 5', + y: 34, + c: 'Green slime' + }, + { + x: 'Round 5', + y: 64, + c: 'Flying pig' + }, + { + x: 'Round 6', + y: 44, + c: 'Green slime' + }, + { + x: 'Round 6', + y: 72, + c: 'Flying pig' + }, + { + x: 'Round 7', + y: 38, + c: 'Green slime' + }, + { + x: 'Round 7', + y: 65, + c: 'Flying pig' + }, + { + x: 'Round 8', + y: 24, + c: 'Green slime' + }, + { + x: 'Round 8', + y: 70, + c: 'Flying pig' + }, + { + x: 'Round 9', + y: 28, + c: 'Green slime' + }, + { + x: 'Round 9', + y: 62, + c: 'Flying pig' + } + ] + } + ], + legends: { + visible: true, + orient: 'bottom' + }, + axes: [ + { + orient: 'left', + max: 100 + }, + { + orient: 'bottom' + }, + { + orient: 'right', + max: 100 + } + ], + xField: 'x', + yField: 'y', + seriesField: 'c', + point: { + style: { + size: 5 + }, + state: { + dimension_hover: { + size: 10 + } + } + }, + crosshair: { + xField: { + visible: true, + line: { + type: 'line', + style: { + lineWidth: 1, + opacity: 1, + strokeOpacity: 1, + stroke: '#000', + lineDash: [2, 2] + } + }, + bindingAxesIndex: [1], + defaultSelect: { + axisIndex: 1, + datum: 'Round 6' + }, + label: { + visible: true + } + }, + yField: { + visible: true, + bindingAxesIndex: [0, 2], + defaultSelect: { + axisIndex: 2, + datum: 40 + }, + label: { + visible: true + }, + line: { + style: { + lineWidth: 1, + opacity: 1, + strokeOpacity: 1, + stroke: '#000', + lineDash: [2, 2] + } + } + } + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'crosshair'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-default.mjs b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-default.mjs new file mode 100644 index 0000000000..aa29465753 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-default.mjs @@ -0,0 +1,123 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6566fdc5fb5448e50e23a367 + * 验证目的:极坐标默认选中的径向和角度 crosshair。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'radar', + data: { + id: 'radar', + values: [ + { theta: '0', type: 'A', r: '873' }, + { theta: '1', type: 'A', r: '713' }, + { theta: '2', type: 'A', r: '742' }, + { theta: '3', type: 'A', r: '861' }, + { theta: '4', type: 'A', r: '815' }, + { theta: '5', type: 'A', r: '660' }, + { theta: '6', type: 'A', r: '795' }, + { theta: '7', type: 'A', r: '722' }, + { theta: '8', type: 'A', r: '818' }, + { theta: '9', type: 'A', r: '666' }, + { theta: '0', type: 'B', r: '896' }, + { theta: '1', type: 'B', r: '814' }, + { theta: '2', type: 'B', r: '856' }, + { theta: '3', type: 'B', r: '840' }, + { theta: '4', type: 'B', r: '634' }, + { theta: '5', type: 'B', r: '616' }, + { theta: '6', type: 'B', r: '652' }, + { theta: '7', type: 'B', r: '831' }, + { theta: '8', type: 'B', r: '614' }, + { theta: '9', type: 'B', r: '648' } + ] + }, + categoryField: 'theta', + valueField: 'r', + seriesField: 'type', + groupBy: 'type', + startAngle: 90, + axes: [ + { + orient: 'angle', + domainLine: { visible: true, smooth: false }, + tick: { visible: true }, + grid: { visible: true }, + label: { visible: true } + }, + { + orient: 'radius', + label: { visible: true }, + grid: { visible: true, smooth: false }, + domainLine: { visible: true, smooth: false }, + tick: { visible: true } + } + ], + crosshair: { + categoryField: { + visible: true, + line: { + type: 'rect', + style: { + stroke: 'red' + } + }, + label: { + visible: true, + style: { + fontSize: 14 + }, + labelBackground: { + style: { + fill: 'pink' + } + } + }, + bindingAxesIndex: [0], + defaultSelect: { + axisIndex: 0, + datum: '6' + } + }, + valueField: { + visible: true, + line: { + style: { + stroke: 'red' + } + }, + label: { + visible: true, + style: { + fill: 'blue' + }, + labelBackground: { + style: { + fill: 'pink' + } + } + }, + bindingAxesIndex: [1], + defaultSelect: { + axisIndex: 1, + datum: 550 + } + } + } + // tooltip: { + // visible: false + // } + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'crosshair'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-formatters.mjs b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-formatters.mjs new file mode 100644 index 0000000000..dbb6395341 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/crosshair/crosshair-polar-formatters.mjs @@ -0,0 +1,108 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65d850f26210ca00ad4a358d + * 验证目的:极坐标十字线分类和数值 formatter 的真实悬停输出。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'radar', + data: [ + { + id: 'radar', + values: [ + { name: 'Openlane', value1: 160.2, value2: 66.9 }, + { name: 'Yearin', value1: 150.1, value2: 50.5 }, + { name: 'Goodsilron', value1: 120.7, value2: 32.3 }, + { name: 'Condax', value1: 89.4, value2: 74.5 }, + { name: 'Opentech', value1: 78.5, value2: 29.7 }, + { name: 'Golddex', value1: 77.6, value2: 102.2 }, + { name: 'Isdom', value1: 69.8, value2: 22.6 }, + { name: 'Plusstrip', value1: 63.6, value2: 45.3 }, + { name: 'Kinnamplus', value1: 59.7, value2: 12.8 }, + { name: 'Zumgoity', value1: 54.3, value2: 19.6 }, + { name: 'Stanredtax', value1: 52.9, value2: 96.3 }, + { name: 'Conecom', value1: 42.9, value2: 11.9 }, + { name: 'Zencorporation', value1: 40.9, value2: 16.8 }, + { name: 'Iselectrics', value1: 39.2, value2: 9.9 }, + { name: 'Treequote', value1: 36.6, value2: 36.9 }, + { name: 'Sumace', value1: 34.8, value2: 14.6 }, + { name: 'Lexiqvolax', value1: 32.1, value2: 35.6 }, + { name: 'Sunnamplex', value1: 31.8, value2: 5.9 }, + { name: 'Faxquote', value1: 29.3, value2: 14.7 }, + { name: 'Donware', value1: 23.0, value2: 2.8 }, + { name: 'Warephase', value1: 21.5, value2: 12.1 }, + { name: 'Donquadtech', value1: 19.7, value2: 10.8 }, + { name: 'Nam-zim', value1: 15.5, value2: 4.1 }, + { name: 'Y-corporation', value1: 14.2, value2: 11.3 } + ], + transforms: [{ type: 'fold', options: { key: 'type', value: 'value', fields: ['value1', 'value2'] } }] + } + ], + categoryField: 'name', + valueField: 'value', + seriesField: 'type', + innerRadius: 0.3, + outerRadius: 0.9, + stack: true, + area: { visible: true }, + point: { visible: false }, + axes: [ + { + orient: 'angle', + label: { formatter: `{label} ttt` }, + domainLine: { style: { lineDash: [2, 2] } }, + grid: { style: { lineDash: [2, 2] } }, + tick: { visible: false } + }, + { + orient: 'radius', + grid: { smooth: true, style: { lineDash: [2, 2] } }, + label: { visible: true, inside: true, formatter: `{label:.1f}` } + } + ], + crosshair: { + categoryField: { + visible: true, + line: { style: { stroke: '#000', lineWidth: 1, opacity: 1, lineDash: [4, 4] } }, + label: { visible: true, formatter: `{label} ttt` } + }, + valueField: { + visible: true, + line: { smooth: true, style: { stroke: '#000', lineWidth: 1, opacity: 1, lineDash: [4, 4] } }, + label: { visible: true, formatter: `{label:.3f}` } + } + }, + legends: { visible: true } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'area'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(() => { + window.__crosshairTexts = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('crosshair') && g.attribute.visible !== false, true) + .flatMap(g => g.findAll?.(x => x.type === 'text', true) ?? []) + .map(g => String(g.attribute.text)); + }); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const texts = window.__crosshairTexts; + if (!texts?.some(t => t.endsWith(' ttt')) || !texts.some(t => /^-?\d+\.\d{3}$/.test(t))) + throw Error('极坐标十字线未应用分类/三位小数格式'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-drag.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-drag.mjs new file mode 100644 index 0000000000..9e5bcb7182 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-drag.mjs @@ -0,0 +1,46 @@ +import { graphicCenter } from '../../../helpers.mjs'; + +/** + * 验证目的:拖动后的可视范围(datazoom)。 + * 图表类型:bar。 + * 关键配置:dataZoom[].start、dataZoom[].end、dataZoom[].filterMode。 + * 场景条件:固定十二条数据,初始范围为完整区间。 + * 最终检查:拖动起始手柄,验证 start 大于 0.1 且可视数据少于十二条。 + * 覆盖边界:不验证滚动条、缩放后更新数据或全部交互组合。 + */ +export default { + createSpec() { + // 给缩放控件留出明确的初始范围。 + return { + type: 'bar', + data: { + id: 'data', + values: Array.from({ length: 12 }, (_, i) => ({ x: `M${i + 1}`, y: 10 + ((i * 13) % 40) })) + }, + xField: 'x', + yField: 'y', + dataZoom: [{ orient: 'bottom', start: 0, end: 1, filterMode: 'filter' }] + }; + }, + async exercise(page) { + // 监听公开事件,只有拖动真正改变范围才完成测试。 + await page.evaluate(() => { + window.__visualChart.on('dataZoomChange', event => { + window.__zoomResult = event.value; + }); + }); + const point = await graphicCenter(page, 'startHandler'); + await page.mouse.move(point.x, point.y); + await page.mouse.down(); + await page.mouse.move(point.x + 180, point.y, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 缩放事件和可视数据必须均已改变。 + await page.waitForFunction(() => window.__zoomResult?.start > 0.1); + await page.waitForFunction( + () => window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData.length < 12 + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-min-max-span.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-min-max-span.mjs new file mode 100644 index 0000000000..84623609b0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-min-max-span.mjs @@ -0,0 +1,177 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 6511e1ee172872291c4615d5 + * 验证目的:来源 DataZoom 拖动及有效范围约束。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { State: 'WY', 年龄段: '小于5岁adwadwdaw', 人口数量: 25635 }, + { State: 'WY', 年龄段: '5至13岁', 人口数量: 1890 }, + { State: 'WY', 年龄段: '14至17岁', 人口数量: 9314 }, + { State: 'WY', 年龄段: '18至19岁', 人口数量: 9314 }, + { State: 'WY', 年龄段: '19至20岁', 人口数量: 9314 }, + { State: 'WY', 年龄段: '21至22岁', 人口数量: 9314 }, + { State: 'WY', 年龄段: '23至24岁', 人口数量: 9314 }, + { State: 'WY', 年龄段: '25至26岁', 人口数量: 9314 }, + { State: 'DC', 年龄段: '小于5岁', 人口数量: 30352 }, + { State: 'DC', 年龄段: '5至13岁', 人口数量: 20439 }, + { State: 'DC', 年龄段: '14至17岁', 人口数量: 10225 }, + { State: 'DC', 年龄段: '18至19岁', 人口数量: 10225 }, + { State: 'DC', 年龄段: '19至20岁', 人口数量: 10225 }, + { State: 'DC', 年龄段: '21至22岁', 人口数量: 10225 }, + { State: 'DC', 年龄段: '23至24岁', 人口数量: 10225 }, + { State: 'DC', 年龄段: '25至26岁', 人口数量: 10225 }, + { State: 'VT', 年龄段: '小于5岁', 人口数量: 38253 }, + { State: 'VT', 年龄段: '5至13岁', 人口数量: 42538 }, + { State: 'VT', 年龄段: '14至17岁', 人口数量: 15757 }, + { State: 'VT', 年龄段: '18至19岁', 人口数量: 15757 }, + { State: 'VT', 年龄段: '19至20岁', 人口数量: 15757 }, + { State: 'VT', 年龄段: '21至22岁', 人口数量: 15757 }, + { State: 'VT', 年龄段: '23至24岁', 人口数量: 15757 }, + { State: 'VT', 年龄段: '25至26岁', 人口数量: 15757 }, + { State: 'ND', 年龄段: '小于5岁', 人口数量: 51896 }, + { State: 'ND', 年龄段: '5至13岁', 人口数量: 67358 }, + { State: 'ND', 年龄段: '14至17岁', 人口数量: 18794 }, + { State: 'ND', 年龄段: '18至19岁', 人口数量: 18794 }, + { State: 'ND', 年龄段: '19至20岁', 人口数量: 18794 }, + { State: 'ND', 年龄段: '21至22岁', 人口数量: 18794 }, + { State: 'ND', 年龄段: '23至24岁', 人口数量: 18794 }, + { State: 'ND', 年龄段: '25至26岁', 人口数量: 18794 }, + { State: 'AK', 年龄段: '小于5岁', 人口数量: 72083 }, + { State: 'AK', 年龄段: '5至13岁', 人口数量: 85640 }, + { State: 'AK', 年龄段: '14至17岁', 人口数量: 22153 }, + { State: 'AK', 年龄段: '18至19岁', 人口数量: 22153 }, + { State: 'AK', 年龄段: '19至20岁', 人口数量: 22153 }, + { State: 'AK', 年龄段: '21至22岁', 人口数量: 22153 }, + { State: 'AK', 年龄段: '23至24岁', 人口数量: 22153 }, + { State: 'AK', 年龄段: '25至26岁', 人口数量: 22153 } + ] + } + ], + padding: { top: 30 }, + xField: ['State', '年龄段'], + yField: '人口数量', + seriesField: '年龄段', + legends: { + visible: true, + orient: 'left', + type: 'discrete', + maxRow: 1, + padding: { right: 12 }, + pager: { + padding: 0, + space: 0, + handler: { + space: 4, + preShape: '', + nextShape: '', + style: { fill: '#8F959E', size: 7 }, + state: { hover: { fill: '#336DF4' }, disable: { fill: '#BBBFC4' } } + }, + textStyle: { fill: '#8F959E', fontSize: 12, lineHeight: 20 } + }, + item: { + visible: true, + spaceCol: 16, + background: { state: { selectedHover: { fill: '#E22E28', fillOpacity: '0.1' } } }, + label: { formatMethod: text => text, style: { fontSize: 12, maxLineWidth: 120 } }, + shape: { space: 2, style: { symbolType: 'circle', size: 8 } } + } + }, + bar: { state: { hover: { stroke: '#000', lineWidth: 1 } } }, + label: { + visible: true, + position: 'inside', + style: { fontSize: 12, lineHeight: 20, fontWeight: 700, fontFamily: 'DIN Alternate' } + }, + tooltip: { + minWidth: 120, + dimension: { + content: { key: datum => datum.年龄段 + ':', value: datum => datum.人口数量, shapeType: 'circle' } + }, + style: { + spaceRow: 2, + keyLabel: { lineHeight: 20, spacing: 0, fontSize: 12 }, + valueLabel: { lineHeight: 20, fontSize: 12 }, + titleLabel: { fontSize: 12, lineHeight: 20, fontColor: '#1F2329' }, + panel: { + padding: { top: 10, bottom: 10, left: 12, right: 12 }, + border: { color: '#EFF0F1', width: 1, radius: 6 }, + shadow: { x: 0, y: 4, blur: 16, spread: 4, color: '#1F232908' } + }, + shape: { size: 8, spacing: 5 } + } + }, + dataZoom: [{ orient: 'bottom', start: 0, end: 0.2, minSpan: 0.1, maxSpan: 0.5 }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__zoomEvents = []; + window.__zoomBefore = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + c.on('dataZoomChange', e => window.__zoomEvents.push({ ...e.value })); + }); + const p = await graphicCenter(page, 'endHandler'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 160, p.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + events = window.__zoomEvents, + r = events?.at(-1), + cfg = c.getSpec().dataZoom[0]; + if (!r || !Number.isFinite(r.start) || !Number.isFinite(r.end) || r.end <= r.start) + throw Error('未产生有效范围变化'); + const span = r.end - r.start; + if ( + (cfg.minSpan !== undefined && span < cfg.minSpan - 1e-6) || + (cfg.maxSpan !== undefined && span > cfg.maxSpan + 1e-6) + ) + throw Error('范围违反来源 minSpan/maxSpan'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (actual === window.__zoomBefore) throw Error('缩放没有影响实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview-brush.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview-brush.mjs new file mode 100644 index 0000000000..c29dd06e16 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview-brush.mjs @@ -0,0 +1,99 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65f7f08fe17cd600d4f92832 + * 验证目的:来源 DataZoom 拖动及有效范围约束。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + autoFit: true, + data: [ + { + id: 'id0', + values: [ + { x: 1, y: 20 }, + { x: 2, y: 40 }, + { x: 3, y: 30 }, + { x: 4, y: 50 }, + { x: 5, y: 40 } + ] + } + ], + width: 600, + height: 600, + series: [{ type: 'line', dataIndex: 0, xField: 'x', yField: 'y' }], + dataZoom: [{ orient: 'bottom', start: 0, delayType: 'throttle', brushSelect: true, showDetail: true }], + axes: [{ orient: 'left' }, { orient: 'bottom' }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__zoomEvents = []; + window.__zoomBefore = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + c.on('dataZoomChange', e => window.__zoomEvents.push({ ...e.value })); + }); + const p = await graphicCenter(page, 'background'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 160, p.y + 0, { steps: 12 }); + // 保留源 throttle;松开前等待最后一次范围事件,避免尚未处理的尾端移动被丢弃。 + const end = await page.evaluate(() => { + const background = window.__visualChart.getStage().find(g => g.name === 'background', true); + return Math.min(1, 0.5 + 160 / background.attribute.width); + }); + await page.waitForFunction(end => Math.abs(window.__zoomEvents?.at(-1)?.end - end) < 1e-8, end); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + events = window.__zoomEvents, + r = events?.at(-1), + cfg = c.getSpec().dataZoom[0]; + if (!r || !Number.isFinite(r.start) || !Number.isFinite(r.end) || r.end <= r.start) + throw Error('未产生有效范围变化'); + const span = r.end - r.start; + if ( + (cfg.minSpan !== undefined && span < cfg.minSpan - 1e-6) || + (cfg.maxSpan !== undefined && span > cfg.maxSpan + 1e-6) + ) + throw Error('范围违反来源 minSpan/maxSpan'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (actual === window.__zoomBefore) throw Error('缩放没有影响实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview.mjs new file mode 100644 index 0000000000..99676e6e03 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-preview.mjs @@ -0,0 +1,267 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65558da7b53f99008e8e6ddc + * 验证目的:dataZoom 预览图的初始范围和布局。 + * 保留条件:title, xField, yField, seriesField, legends, crosshair, axes, dataZoom;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主;限定 width=400,使来源的 auto/minBandSize 条件实际显示 dataZoom。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + width: 400, + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true, + label: { + visible: true + } + }, + yField: { + visible: true, + label: { + visible: true + } + } + }, + axes: [ + { + orient: 'bottom', + type: 'band', + minBandSize: 50, + maxBandSize: 100, + autoRegionSize: true + }, + { + orient: 'left', + type: 'linear' + }, + { + orient: 'right', + type: 'linear' + } + ], + dataZoom: [ + { + orient: 'bottom', + start: 0, + filterMode: 'axis', + axisIndex: 0, + auto: true, + ignoreBandSize: true + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const component = window.__visualChart.getStage().find(g => g.name === 'dataZoom', true); + if (!component || component.attribute.visible === false || component.globalAABBBounds.width() <= 0) + throw new Error('自动 dataZoom 未显示'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-time-span.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-time-span.mjs new file mode 100644 index 0000000000..c8ce2fa5b9 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-time-span.mjs @@ -0,0 +1,222 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65f7b21549222d00d0762038 + * 验证目的:来源 DataZoom 拖动及有效范围约束。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + color: [ + '#3855df', + '#ffc52b', + '#5ecf78', + '#fb7a00', + '#0acffd', + '#217dfd', + '#98dd61', + '#3150e0', + '#714efd', + '#0bcfff', + '#3d0dde', + '#ffc527', + '#f5c13f', + '#fb7a08', + '#95d8fd' + ], + type: 'rangeColumn', + direction: 'horizontal', + yField: 'type', + minField: 'start_time', + maxField: 'end_time', + seriesField: 'color', + dataZoom: [ + { + orient: 'bottom', + height: 20, + start: 0.1, + endValue: 1681956000, + maxSpan: 0.8, + filterMode: 'axis', + brushSelect: false, + startText: { formatMethod: text => Math.floor(text) }, + endText: { formatMethod: text => Math.floor(text) } + } + ], + axes: [ + { orient: 'left', type: 'band', bandPadding: 0.5, visible: false }, + { type: 'time', orient: 'bottom', layers: [{ tickStep: 28800, timeFormat: '%Y%m%d %H:%M' }] } + ], + title: { + textStyle: { + character: [ + { text: 'Time-Consuming Distribution', fontWeight: 400, fill: '#222' }, + { text: 'Show the SQL distribution of TOP 100', fontWeight: 200, fontSize: 10, fill: '#555' } + ] + } + }, + tooltip: { + visible: true, + dimension: { visible: false }, + mark: { + title: { key: 'Query ID', value: datum => 'Query ID: ' + datum['id'] }, + content: [ + { key: 'Time Consuming', value: datum => datum['useTime'] }, + { key: 'start time', value: datum => datum['start_time'] }, + { key: 'end time', value: datum => datum['end_time'] } + ] + } + }, + data: [ + { + id: 'data0', + values: [ + { + start_time: 1681926000, + end_time: 1681927200, + type: 'TOP 1', + color: 'A', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681926000, + end_time: 1681959600, + type: 'TOP 2', + color: 'B', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681925400, + end_time: 1681974000, + type: 'TOP 3', + color: 'C', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681924800, + end_time: 1681933200, + type: 'TOP 4', + color: 'D', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681959600, + end_time: 1681963200, + type: 'TOP 5', + color: 'E', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681970400, + end_time: 1681971000, + type: 'TOP 5', + color: 'F', + id: 'a90292870-9282', + useTime: '100ms' + }, + { start_time: 1681992000, end_time: 1681992600, type: 'TOP 5', color: 'G', useTime: '100ms' }, + { + start_time: 1681956000, + end_time: 1681963200, + type: 'TOP 6', + color: 'H', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681990200, + end_time: 1681993800, + type: 'TOP 7', + color: 'I', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681948800, + end_time: 1681959600, + type: 'TOP 8', + color: 'J', + id: 'a90292870-9282', + useTime: '100ms' + }, + { + start_time: 1681945200, + end_time: 1681956000, + type: 'TOP 9', + color: 'K', + id: 'a90292870-9282', + useTime: '100ms' + } + ].reverse() + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__zoomEvents = []; + window.__zoomBefore = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + c.on('dataZoomChange', e => window.__zoomEvents.push({ ...e.value })); + }); + const p = await graphicCenter(page, 'endHandler'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 160, p.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + events = window.__zoomEvents, + r = events?.at(-1), + cfg = c.getSpec().dataZoom[0]; + if (!r || !Number.isFinite(r.start) || !Number.isFinite(r.end) || r.end <= r.start) + throw Error('未产生有效范围变化'); + const span = r.end - r.start; + if ( + (cfg.minSpan !== undefined && span < cfg.minSpan - 1e-6) || + (cfg.maxSpan !== undefined && span > cfg.maxSpan + 1e-6) + ) + throw Error('范围违反来源 minSpan/maxSpan'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (actual === window.__zoomBefore) throw Error('缩放没有影响实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-fixed-span.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-fixed-span.mjs new file mode 100644 index 0000000000..b046f481ea --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-fixed-span.mjs @@ -0,0 +1,101 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 6620bfc6b19f5100ced6a52e + * 验证目的:来源 DataZoom 拖动及有效范围约束。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { year: '2000', sales: 22 }, + { year: '2001', sales: 13 }, + { year: '2002', sales: 25 }, + { year: '2003', sales: 29 }, + { year: '2004', sales: 38 }, + { year: '2005', sales: 49 }, + { year: '2006', sales: 58 }, + { year: '2007', sales: 29 }, + { year: '2008', sales: 78 }, + { year: '2009', sales: 19 }, + { year: '2010', sales: 23 }, + { year: '2011', sales: 20 }, + { year: '2012', sales: 98 }, + { year: '2013', sales: 49 }, + { year: '2014', sales: 28 } + ] + } + ], + direction: 'horizontal', + yField: 'year', + xField: 'sales', + dataZoom: [{ orient: 'right', roam: true, start: 0, end: 0.2, minSpan: 0.2, maxSpan: 0.2 }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const c = window.__visualChart; + window.__zoomEvents = []; + window.__zoomBefore = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + c.on('dataZoomChange', e => window.__zoomEvents.push({ ...e.value })); + }); + const p = await graphicCenter(page, 'selectedBackground'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 0, p.y + 140, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + events = window.__zoomEvents, + r = events?.at(-1), + cfg = c.getSpec().dataZoom[0]; + if (!r || !Number.isFinite(r.start) || !Number.isFinite(r.end) || r.end <= r.start) + throw Error('未产生有效范围变化'); + const span = r.end - r.start; + if ( + (cfg.minSpan !== undefined && span < cfg.minSpan - 1e-6) || + (cfg.maxSpan !== undefined && span > cfg.maxSpan + 1e-6) + ) + throw Error('范围违反来源 minSpan/maxSpan'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (actual === window.__zoomBefore) throw Error('缩放没有影响实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-initial-range.mjs b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-initial-range.mjs new file mode 100644 index 0000000000..03a2e9671c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/datazoom/datazoom-vertical-initial-range.mjs @@ -0,0 +1,280 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65f7f7a049222d00d0762041 + * 验证目的:纵向 dataZoom 初始范围与轴窗口。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:只测初始范围,源 maxSpan 和拖动限制尚未做行为验证。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'area', + direction: 'horizontal', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + yField: 'type', + xField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true, + label: { + visible: true + } + }, + yField: { + visible: true, + label: { + visible: true + } + } + }, + axes: [ + { + orient: 'left', + type: 'band', + minBandSize: 50, + maxBandSize: 100, + autoRegionSize: true, + inverse: true + }, + { + orient: 'top', + type: 'linear' + }, + { + orient: 'bottom', + type: 'linear' + } + ], + dataZoom: [ + { + orient: 'right', + start: 0, + end: 0.5, + maxSpan: 0.8, + filterMode: 'axis', + axisIndex: 0, + auto: true + } + ], + tooltip: { + dimension: { + content: [ + { + key: 'type11111', + value: 'type11111', + shapeFill: 'transparent', + shapeLineWidth: 0.2 + } + ], + position: 'tl', + positionMode: 'pointer' + }, + mark: { + position: 'top' + } + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/indicator/indicator-autofit.mjs b/packages/vchart/__tests__/visual/cases/components/indicator/indicator-autofit.mjs new file mode 100644 index 0000000000..1636676cf6 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/indicator/indicator-autofit.mjs @@ -0,0 +1,139 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65a0e60c918eea2d9742291c + * 验证目的:环图指标文字自动适应。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const data = [ + { type: 'oxygen', value: '46.60', formula: 'O', texture: 'circle' }, + { type: 'silicon', value: '27.72', formula: 'Si', texture: 'horizontal-line' }, + { type: 'aluminum', value: '8.13', formula: 'Al', texture: 'vertical-line' }, + { type: 'iron', value: '5', formula: 'Fe', texture: 'rect' }, + { type: 'calcium', value: '3.63', formula: 'Ca', texture: 'grid' }, + { type: 'sodium', value: '2.83', formula: 'Na', texture: 'bias-rl' }, + { type: 'potassium', value: '2.59', formula: 'K', texture: 'diamond' }, + { type: 'others', value: '3.5', formula: 'Others', texture: 'bias-lr' } + ]; + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: data + } + ], + outerRadius: 0.8, + innerRadius: 0.5, + padAngle: 0.6, + valueField: 'value', + categoryField: 'type', + pie: { + style: { + cornerRadius: 10, + texture: datum => datum['texture'] + }, + state: { + hover: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + }, + selected: { + outerRadius: 0.85, + stroke: '#000', + lineWidth: 1 + } + } + }, + title: { + visible: true, + text: 'Statistics of Surface Element Content' + }, + indicator: { + visible: true, + trigger: 'hover', + limitRatio: 0.5, + title: { + visible: true, + autoFit: true, + fitStrategy: 'inscribed', + style: { + fontWeight: 'bolder', + fontFamily: 'Times New Roman', + fill: '#888', + text: datum => { + const d = datum ?? data[0]; + return d['formula']; + } + } + }, + content: [ + { + visible: true, + autoFit: true, + fitStrategy: 'inscribed', + style: { + fontSize: 20, + fill: 'orange', + fontWeight: 'bolder', + fontFamily: 'Times New Roman', + text: datum => { + const d = datum ?? data[0]; + return d['type']; + } + } + }, + { + visible: true, + autoFit: true, + fitStrategy: 'inscribed', + style: { + fontSize: 18, + fill: 'orange', + fontFamily: 'Times New Roman', + text: datum => { + const d = datum ?? data[0]; + return d['value'] + '%'; + } + } + } + ] + }, + legends: { + visible: true, + orient: 'left', + item: { + shape: { + style: { + symbolType: 'circle', + texture: datum => datum['texture'] + } + } + } + }, + tooltip: { + mark: { + content: [ + { + key: datum => datum['type'], + value: datum => datum['value'] + '%' + } + ] + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'indicator'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/indicator/indicator-richtext-click.mjs b/packages/vchart/__tests__/visual/cases/components/indicator/indicator-richtext-click.mjs new file mode 100644 index 0000000000..7856d5ae2a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/indicator/indicator-richtext-click.mjs @@ -0,0 +1,101 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 655ad1e99ea0da0095642841 + * 验证目的:饼图选择触发富文本指标及对应数值。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'pie', + data: [ + { + values: [ + { value: '159', type: 'Tradition Industries', percentage: '71.6%' }, + { value: '50', type: 'Business Companies', percentage: '22.5%' }, + { value: '13', type: 'Customer-facing Companies', percentage: '5.9%' } + ] + } + ], + radius: 0.8, + innerRadius: 0.5, + valueField: 'value', + categoryField: 'type', + label: { + visible: true, + style: { fontSize: 16 }, + line: { style: {}, line1MinLength: 30 }, + layout: { align: 'edge' } + }, + pie: { state: { selected: { outerRadius: 0.85 } } }, + indicator: { + visible: true, + fixed: false, + trigger: 'select', + gap: 10, + title: { + autoLimit: true, + style: { + fontSize: 16, + type: 'rich', + text: datum => { + if (!datum) { + return ''; + } + return [ + { text: 'type:', fontWeight: 'bold', fontSize: 20, fill: '#3f51b5' }, + { text: datum.type, fontStyle: 'italic', textDecoration: 'underline', fill: '#3f51b5' } + ]; + } + } + }, + content: [ + { field: 'value', style: { fontSize: 42, fontWeight: 'bolder' } }, + { field: 'percentage', style: { fontSize: 20 } } + ] + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'pie'); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0], + g = s + .getSeriesMark() + .getGraphics() + .find(g => g.currentStates?.includes('selected')); + window.__indicatorValue = g?.context.data[0][s.getSpec().valueField]; + }); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const value = window.__indicatorValue, + c = window.__visualChart; + if (value === undefined) throw Error('饼图未选中'); + const indicators = c + .getStage() + .findAll(g => g.name?.includes('indicator') && g.attribute.visible !== false, true); + if ( + !indicators.length || + !indicators.some( + g => + JSON.stringify(g.attribute).includes(String(value)) || + g.findAll?.(x => JSON.stringify(x.attribute).includes(String(value)), true).length + ) + ) + throw Error('指标未显示选中值'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/bar-gradient-label.mjs b/packages/vchart/__tests__/visual/cases/components/label/bar-gradient-label.mjs new file mode 100644 index 0000000000..9b08f8aa2f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/bar-gradient-label.mjs @@ -0,0 +1,167 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 673dbf5979679700b2633f0e + * 验证目的:透明渐变柱与标签布局。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + type: 'Autocracies', + year: '1930', + value: 129 + }, + { + type: 'Autocracies', + year: '1940', + value: 133 + }, + { + type: 'Autocracies', + year: '1950', + value: 130 + }, + { + type: 'Autocracies', + year: '1960', + value: 126 + }, + { + type: 'Autocracies', + year: '1970', + value: 117 + }, + { + type: 'Autocracies', + year: '1980', + value: 114 + }, + { + type: 'Autocracies', + year: '1990', + value: 111 + }, + { + type: 'Autocracies', + year: '2000', + value: 89 + }, + { + type: 'Autocracies', + year: '2010', + value: 80 + }, + { + type: 'Autocracies', + year: '2018', + value: 80 + }, + { + type: 'Democracies', + year: '1930', + value: 22 + }, + { + type: 'Democracies', + year: '1940', + value: 13 + }, + { + type: 'Democracies', + year: '1950', + value: 25 + }, + { + type: 'Democracies', + year: '1960', + value: 29 + }, + { + type: 'Democracies', + year: '1970', + value: 38 + }, + { + type: 'Democracies', + year: '1980', + value: 41 + }, + { + type: 'Democracies', + year: '1990', + value: 57 + }, + { + type: 'Democracies', + year: '2000', + value: 87 + }, + { + type: 'Democracies', + year: '2010', + value: 98 + }, + { + type: 'Democracies', + year: '2018', + value: 99 + } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + legends: { + visible: true, + orient: 'top', + position: 'start' + }, + theme: { + background: 'transparent', + colorScheme: { + default: ['red', 'green'] + } + }, + bar: { + style: { + fill: { + gradient: 'linear', + x0: 0, + y0: 0, + x1: 0, + y1: 1, + stops: [ + { + offset: 0, + opacity: 1 + }, + { + offset: 1, + opacity: 0 + } + ] + } + } + }, + label: { + visible: true, + position: 'inside-top' + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/grid-line-label-layout.mjs b/packages/vchart/__tests__/visual/cases/components/label/grid-line-label-layout.mjs new file mode 100644 index 0000000000..5484526044 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/grid-line-label-layout.mjs @@ -0,0 +1,581 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0552a1e9eec95f9ec2 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:三区域折线标签与独立轴布局。 + * 保留条件:三区域五系列、八轴、labelLayout=region、末系列 top/overlap=false。 + * 改写说明:仅替换说明中提及的通用类目或标题文本;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'common', + stackField: 'stack', + seriesField: 'type', + layout: { + type: 'grid', + col: 9, + row: 9, + colWidth: [ + { + index: 0, + size: 50 + }, + { + index: 4, + size: 50 + }, + { + index: 8, + size: 50 + } + ], + rowHeight: [ + { + index: 0, + size: 50 + }, + { + index: 4, + size: 50 + }, + { + index: 8, + size: 50 + } + ], + elements: [ + { + modelId: 'legend0', + col: 2, + row: 3 + }, + { + modelId: 'legend1', + col: 6, + row: 3 + }, + { + modelId: 'legend2', + col: 2, + row: 7 + }, + { + modelId: 'region0', + col: 2, + row: 1 + }, + { + modelId: 'region1', + col: 6, + row: 1 + }, + { + modelId: 'region2', + col: 2, + row: 5 + }, + { + modelId: 'axesLeft0', + col: 1, + row: 1 + }, + { + modelId: 'axesLeft1', + col: 5, + row: 1 + }, + { + modelId: 'axesRight0', + col: 3, + row: 1 + }, + { + modelId: 'axesRight1', + col: 7, + row: 1 + }, + { + modelId: 'axesRight2', + col: 3, + row: 5 + }, + { + modelId: 'axesBottom0', + col: 2, + row: 2 + }, + { + modelId: 'axesBottom1', + col: 6, + row: 2 + }, + { + modelId: 'axesBottom2', + col: 2, + row: 6 + } + ] + }, + region: [ + { + id: 'region0' + }, + { + id: 'region1' + }, + { + id: 'region2' + } + ], + legends: [ + { + visible: true, + orient: 'bottom', + id: 'legend0', + position: 'start', + regionIndex: [0], + item: { + visible: true + } + }, + { + visible: true, + orient: 'bottom', + id: 'legend1', + position: 'start', + regionIndex: [1], + item: { + visible: true + } + }, + { + visible: true, + orient: 'bottom', + id: 'legend2', + position: 'start', + regionIndex: [2], + item: { + visible: true + } + } + ], + labelLayout: 'region', + series: [ + { + type: 'line', + data: { + id: 'A', + values: [ + { + x: 0, + y: 13, + type: 'A' + }, + { + x: 1, + y: 42, + type: 'A' + }, + { + x: 2, + y: 159, + type: 'A' + }, + { + x: 3, + y: 135, + type: 'A' + }, + { + x: 4, + y: 66, + type: 'A' + }, + { + x: 5, + y: 43, + type: 'A' + }, + { + x: 6, + y: 280, + type: 'A' + }, + { + x: 7, + y: 200, + type: 'A' + }, + { + x: 8, + y: 16, + type: 'A' + }, + { + x: 9, + y: 109, + type: 'A' + } + ] + }, + regionIndex: 0, + label: { + visible: true + }, + id: 'line0', + xField: 'x', + yField: 'y', + seriesField: 'type' + }, + { + type: 'line', + data: { + id: 'B', + values: [ + { + x: 0, + y: 67, + type: 'B' + }, + { + x: 1, + y: 268, + type: 'B' + }, + { + x: 2, + y: 175, + type: 'B' + }, + { + x: 3, + y: 179, + type: 'B' + }, + { + x: 4, + y: 41, + type: 'B' + }, + { + x: 5, + y: 142, + type: 'B' + }, + { + x: 6, + y: 142, + type: 'B' + }, + { + x: 7, + y: 236, + type: 'B' + }, + { + x: 8, + y: 215, + type: 'B' + }, + { + x: 9, + y: 196, + type: 'B' + } + ] + }, + regionIndex: 0, + id: 'line1', + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true + } + }, + { + type: 'line', + data: { + id: 'C', + values: [ + { + x: 0, + y: 55, + type: 'C' + }, + { + x: 1, + y: 136, + type: 'C' + }, + { + x: 2, + y: 265, + type: 'C' + }, + { + x: 3, + y: 106, + type: 'C' + }, + { + x: 4, + y: 154, + type: 'C' + }, + { + x: 5, + y: 86, + type: 'C' + }, + { + x: 6, + y: 89, + type: 'C' + }, + { + x: 7, + y: 104, + type: 'C' + }, + { + x: 8, + y: 100, + type: 'C' + }, + { + x: 9, + y: 166, + type: 'C' + } + ] + }, + regionIndex: 1, + id: 'line2', + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true + } + }, + { + type: 'line', + data: { + id: 'D', + values: [ + { + x: 0, + y: 124, + type: 'D' + }, + { + x: 1, + y: 242, + type: 'D' + }, + { + x: 2, + y: 182, + type: 'D' + }, + { + x: 3, + y: 179, + type: 'D' + }, + { + x: 4, + y: 143, + type: 'D' + }, + { + x: 5, + y: 291, + type: 'D' + }, + { + x: 6, + y: 248, + type: 'D' + }, + { + x: 7, + y: 90, + type: 'D' + }, + { + x: 8, + y: 74, + type: 'D' + }, + { + x: 9, + y: 291, + type: 'D' + } + ] + }, + regionIndex: 1, + id: 'line3', + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true + } + }, + { + type: 'line', + data: { + id: 'E', + values: [ + { + x: 0, + y: 13, + type: 'E' + }, + { + x: 1, + y: 243, + type: 'E' + }, + { + x: 2, + y: 282, + type: 'E' + }, + { + x: 3, + y: 252, + type: 'E' + }, + { + x: 4, + y: 217, + type: 'E' + }, + { + x: 5, + y: 282, + type: 'E' + }, + { + x: 6, + y: 49, + type: 'E' + }, + { + x: 7, + y: 26, + type: 'E' + }, + { + x: 8, + y: 115, + type: 'E' + }, + { + x: 9, + y: 114, + type: 'E' + } + ] + }, + regionIndex: 2, + id: 'line4', + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true, + position: 'top', + overlap: false + } + } + ], + axes: [ + { + orient: 'left', + regionIndex: 0, + seriesId: 'line0', + id: 'axesLeft0' + }, + { + orient: 'left', + regionIndex: 1, + seriesId: 'line2', + id: 'axesLeft1' + }, + { + orient: 'right', + regionIndex: 0, + seriesId: 'line1', + id: 'axesRight0' + }, + { + orient: 'right', + regionIndex: 1, + seriesId: 'line3', + id: 'axesRight1' + }, + { + orient: 'right', + regionIndex: 2, + seriesId: 'line4', + id: 'axesRight2' + }, + { + orient: 'bottom', + label: { + visible: true + }, + regionIndex: [0], + id: 'axesBottom0' + }, + { + orient: 'bottom', + label: { + visible: true + }, + regionIndex: [1], + id: 'axesBottom1' + }, + { + orient: 'bottom', + title: { + visible: true, + style: { + text: 'Axis 1' + } + }, + regionIndex: [2], + id: 'axesBottom2' + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + await page.evaluate(() => { + const chart = window.__visualChart.getChart(); + if (chart.getAllRegions().length !== 3 || chart.getAllSeries().length !== 5) + throw new Error('网格区域或系列丢失'); + const components = chart.getAllComponents(); + if ( + components.filter(c => c.type.startsWith('cartesianAxis-')).length !== 8 || + components.filter(c => c.type === 'discreteLegend').length !== 3 + ) + throw new Error('网格布局的轴或图例数量改变'); + const regions = ['region0', 'region0', 'region1', 'region1', 'region2']; + if (chart.getAllSeries().some((series, i) => series.getRegion().getSpec().id !== regions[i])) + throw new Error('折线系列的区域绑定改变'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/label-smart-invert.mjs b/packages/vchart/__tests__/visual/cases/components/label/label-smart-invert.mjs new file mode 100644 index 0000000000..9e73fa1cba --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/label-smart-invert.mjs @@ -0,0 +1,143 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 66f3bfb0e905a400f7326aa3 + * 验证目的:柱图外侧标签的智能反色配置。 + * 保留条件:xField, yField, seriesField, legends, label;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + type: 'Autocracies', + year: '1930', + value: 129 + }, + { + type: 'Autocracies', + year: '1940', + value: 133 + }, + { + type: 'Autocracies', + year: '1950', + value: 130 + }, + { + type: 'Autocracies', + year: '1960', + value: 126 + }, + { + type: 'Autocracies', + year: '1970', + value: 117 + }, + { + type: 'Autocracies', + year: '1980', + value: 114 + }, + { + type: 'Autocracies', + year: '1990', + value: 111 + }, + { + type: 'Autocracies', + year: '2000', + value: 89 + }, + { + type: 'Autocracies', + year: '2010', + value: 80 + }, + { + type: 'Autocracies', + year: '2018', + value: 80 + }, + { + type: 'Democracies', + year: '1930', + value: 22 + }, + { + type: 'Democracies', + year: '1940', + value: 13 + }, + { + type: 'Democracies', + year: '1950', + value: 25 + }, + { + type: 'Democracies', + year: '1960', + value: 29 + }, + { + type: 'Democracies', + year: '1970', + value: 38 + }, + { + type: 'Democracies', + year: '1980', + value: 41 + }, + { + type: 'Democracies', + year: '1990', + value: 57 + }, + { + type: 'Democracies', + year: '2000', + value: 87 + }, + { + type: 'Democracies', + year: '2010', + value: 98 + }, + { + type: 'Democracies', + year: '2018', + value: 99 + } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + legends: { + visible: true, + orient: 'top', + position: 'start' + }, + label: { + visible: true, + smartInvert: { + fillStrategy: 'similarBase' + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/label-stack-zero.mjs b/packages/vchart/__tests__/visual/cases/components/label/label-stack-zero.mjs new file mode 100644 index 0000000000..a99eb9b144 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/label-stack-zero.mjs @@ -0,0 +1,205 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0552a1e9eec95f9ec1 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:零值堆叠柱标签与边界移动避让。 + * 保留条件:30 条原始数据、inside、白色描边、bound/moveY 策略。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + stack: true, + data: [ + { + name: 'allData', + values: [ + { + name: 'A', + value: 0.12, + group: '7+' + }, + { + name: 'B', + value: 0.34, + group: '7+' + }, + { + name: 'C', + value: 0.25, + group: '7+' + }, + { + name: 'D', + value: 0.48, + group: '7+' + }, + { + name: 'E', + value: 0.55, + group: '7+' + }, + { + name: 'F', + value: 0.42, + group: '7+' + }, + { + name: 'A', + value: 0.23, + group: '6-7' + }, + { + name: 'B', + value: 0.25, + group: '6-7' + }, + { + name: 'C', + value: 0.18, + group: '6-7' + }, + { + name: 'D', + value: 0.19, + group: '6-7' + }, + { + name: 'E', + value: 0.15, + group: '6-7' + }, + { + name: 'F', + value: 0.12, + group: '6-7' + }, + { + name: 'A', + value: 0.31, + group: '4-5' + }, + { + name: 'B', + value: 0.33, + group: '4-5' + }, + { + name: 'C', + value: 0.4, + group: '4-5' + }, + { + name: 'D', + value: 0.24, + group: '4-5' + }, + { + name: 'E', + value: 0.18, + group: '4-5' + }, + { + name: 'F', + value: 0.2, + group: '4-5' + }, + { + name: 'A', + value: 0.56, + group: '2-3' + }, + { + name: 'B', + value: 0.29, + group: '2-3' + }, + { + name: 'C', + value: 0.15, + group: '2-3' + }, + { + name: 'D', + value: 0.01, + group: '2-3' + }, + { + name: 'E', + value: 0.14, + group: '2-3' + }, + { + name: 'F', + value: 0.16, + group: '2-3' + }, + { + name: 'A', + value: 0.15, + group: '1' + }, + { + name: 'B', + value: 0.11, + group: '1' + }, + { + name: 'C', + value: 0.015, + group: '1' + }, + { + name: 'D', + value: 0.02, + group: '1' + }, + { + name: 'E', + value: 0, + group: '1' + }, + { + name: 'F', + value: 0.05, + group: '1' + } + ] + } + ], + color: ['#009DB5', '#F0B71F', '#EB6F02', '#1E5273', '#3BA140'], + label: { + visible: true, + position: 'inside', + style: { + stroke: 'white', + lineWidth: 2 + }, + overlap: { + strategy: [ + { + type: 'bound', + position: ['top'] + }, + { + type: 'moveY', + offset: [-2, -4, -8, -10, -12] + } + ] + } + }, + type: 'bar', + xField: 'name', + yField: 'value', + seriesField: 'group' + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/line-end-label.mjs b/packages/vchart/__tests__/visual/cases/components/label/line-end-label.mjs new file mode 100644 index 0000000000..11d9cc5dbf --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/line-end-label.mjs @@ -0,0 +1,66 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 67b72068fe6e6600b22e2df8 + * 验证目的:折线末端标签。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + lineLabel: { + visible: true + }, + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value' + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/multi-bar-inside.mjs b/packages/vchart/__tests__/visual/cases/components/label/multi-bar-inside.mjs new file mode 100644 index 0000000000..c1731644f1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/multi-bar-inside.mjs @@ -0,0 +1,114 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 6479a64ac48954008d952da9 + * 迁移方式:原配置迁移。 + * 验证目的:两份数据中的四个分组使用两个柱系列并显示内部标签。 + * 保留条件:两个数据源及 dataIndex、各四行数据、seriesField、inside 标签、600×600。 + * 覆盖边界:不折叠为单柱系列,不合并分组;不宣称复现未提供的历史缺陷。 + */ +export default { + createSpec() { + // 返回原始配置结构的新副本,只移除宿主生命周期依赖。 + return { + type: 'common', + autoFit: true, + data: [ + { + id: 'id0', + values: [ + { + x: 1, + y: 20, + type: 'a' + }, + { + x: 2, + y: 40, + type: 'a' + }, + { + x: 1, + y: 20, + type: 'b' + }, + { + x: 2, + y: 40, + type: 'b' + } + ] + }, + { + id: 'id1', + values: [ + { + x: 1, + y: 20, + type: 'c' + }, + { + x: 2, + y: 40, + type: 'c' + }, + { + x: 1, + y: 20, + type: 'd' + }, + { + x: 2, + y: 40, + type: 'd' + } + ] + } + ], + width: 600, + height: 600, + series: [ + { + type: 'bar', + dataIndex: 0, + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true, + position: 'inside' + } + }, + { + type: 'bar', + dataIndex: 1, + xField: 'x', + yField: 'y', + seriesField: 'type', + label: { + visible: true, + position: 'inside' + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom', + type: 'band' + } + ] + }; + }, + async verify(page) { + // 精确核对所有原始数据、关联和开关,并检查真实系列状态。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'label'); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries(); + if (s.length !== 2 || s.some(s => s.getViewData().latestData.length !== 4)) throw new Error('双数据源或分组丢失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/pie-label-custom-path.mjs b/packages/vchart/__tests__/visual/cases/components/label/pie-label-custom-path.mjs new file mode 100644 index 0000000000..8bab4b45b1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/pie-label-custom-path.mjs @@ -0,0 +1,76 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66abcc3c27f96000ad549eef + * 验证目的:外标签引导线自定义折角路径。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'oxygen', value: '46.60' }, + { type: 'silicon', value: '27.72' }, + { type: 'aluminum', value: '8.13' }, + { type: 'iron', value: '5' }, + { type: 'calcium', value: '3.63' }, + { type: 'sodium', value: '2.83' }, + { type: 'potassium', value: '2.59' }, + { type: 'others', value: '3.5' } + ] + } + ], + outerRadius: 0.8, + valueField: 'value', + categoryField: 'type', + title: { visible: true, text: 'Statistics of Surface Element Content' }, + legends: { visible: true, orient: 'left' }, + label: { + visible: true, + position: 'outside', + style: { angle: 0 }, + line: { + line1MinLength: 40, + line2MinLength: 60, + customShape: (text, attrs, path) => { + console.log('attrs', attrs, path); + let points = attrs.points; + const direction = points[points.length - 1].x - points[0].x > 0 ? -1 : 1; + path.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length - 1; i++) { + const p1 = points[i - 1]; + const p2 = points[i % points.length]; + const p3 = points[(i + 1) % points.length]; + const { x: x1, y: y1 } = p1; + const { x: x2, y: y2 } = p2; + const { x: x3, y: y3 } = p3; + const k1 = (y2 - y1) / (x2 - x1); + const k2 = (y3 - y2) / (x3 - x2); + const deltaX = 3; + const deltaY1 = k1 * deltaX; + const deltaY2 = k2 * deltaX; + path.lineTo(p2.x + direction * deltaX, p2.y + direction * deltaY1); + path.lineTo(p2.x - direction * deltaX, p2.y - direction * deltaY2); + } + path.lineTo(points[points.length - 1].x, points[points.length - 1].y); + return path; + }, + style: { lineWidth: 1, stroke: 'red' } + } + }, + tooltip: { mark: { content: [{ key: datum => datum['type'], value: datum => datum['value'] + '%' }] } } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/label/pie-label.mjs b/packages/vchart/__tests__/visual/cases/components/label/pie-label.mjs new file mode 100644 index 0000000000..18f223d8b5 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/label/pie-label.mjs @@ -0,0 +1,28 @@ +import { verifySpec } from '../../../helpers.mjs'; + +/** + * 验证目的:外侧标签与引导线(pie-label)。 + * 图表类型:pie。 + * 关键配置:label.position、label.visible、outerRadius。 + * 场景条件:固定大扇区与多个小扇区,标签位置为 outside。 + * 最终检查:核对 spec、有效绘制,比较外侧标签与引导线的最终截图。 + * 覆盖边界:不验证标签点击,也不代表全部避让分支。 + */ +export default { + createSpec() { + // 小扇区与大扇区混合验证标签避让。 + return { + type: 'pie', + data: { id: 'data', values: [60, 20, 8, 5, 4, 3].map((value, i) => ({ name: `Category ${i + 1}`, value })) }, + categoryField: 'name', + valueField: 'value', + outerRadius: 0.7, + label: { visible: true, position: 'outside' }, + legends: { visible: false } + }; + }, + async verify(page) { + // 核对目标输入,避免错误 spec 或空数据通过图片对比。 + await verifySpec(page, this.createSpec()); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-clear-selection.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-clear-selection.mjs new file mode 100644 index 0000000000..4027d7e6fe --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-clear-selection.mjs @@ -0,0 +1,249 @@ +import { verifySpec } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0433 + * 验证目的:图例全部取消后恢复选择(空态检查不截图)。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'bar', + data: [ + { + id: 'bar', + values: [ + { + city: 'Shijiazhuang', + type: 'Fruit', + value: 14500 + }, + { + city: 'Shijiazhuang', + type: 'Rice and flour', + value: 8500 + }, + { + city: 'Shijiazhuang', + type: 'Snacks', + value: 10000 + }, + { + city: 'Shijiazhuang', + type: 'Tea', + value: 7000 + }, + { + city: 'Shenzhen', + type: 'Fruit', + value: 9000 + }, + { + city: 'Shenzhen', + type: 'Rice and flour', + value: 8500 + }, + { + city: 'Shenzhen', + type: 'Snacks', + value: 11000 + }, + { + city: 'Shenzhen', + type: 'Tea', + value: 6000 + }, + { + city: 'Wenzhou', + type: 'Fruit', + value: 16000 + }, + { + city: 'Wenzhou', + type: 'Rice and flour', + value: 5000 + }, + { + city: 'Wenzhou', + type: 'Snacks', + value: 6000 + }, + { + city: 'Wenzhou', + type: 'Tea', + value: 10000 + }, + { + city: 'Ningbo', + type: 'Fruit', + value: 14000 + }, + { + city: 'Ningbo', + type: 'Rice and flour', + value: 9000 + }, + { + city: 'Ningbo', + type: 'Snacks', + value: 10000 + }, + { + city: 'Ningbo', + type: 'Tea', + value: 9000 + }, + { + city: 'Wuxi', + type: 'Fruit', + value: 14000 + }, + { + city: 'Wuxi', + type: 'Rice and flour', + value: 9000 + }, + { + city: 'Wuxi', + type: 'Snacks', + value: 10000 + }, + { + city: 'Wuxi', + type: 'Tea', + value: 6000 + }, + { + city: 'Hangzhou', + type: 'Fruit', + value: 9000 + }, + { + city: 'Hangzhou', + type: 'Rice and flour', + value: 8500 + }, + { + city: 'Hangzhou', + type: 'Snacks', + value: 10000 + }, + { + city: 'Hangzhou', + type: 'Tea', + value: 6000 + }, + { + city: 'Beijing', + type: 'Fruit', + value: 17000 + }, + { + city: 'Beijing', + type: 'Rice and flour', + value: 6000 + }, + { + city: 'Beijing', + type: 'Snacks', + value: 7000 + }, + { + city: 'Beijing', + type: 'Tea', + value: 10000 + }, + { + city: 'Shanghai', + type: 'Fruit', + value: 18000 + }, + { + city: 'Shanghai', + type: 'Rice and flour', + value: 11000 + }, + { + city: 'Shanghai', + type: 'Snacks', + value: 15000 + }, + { + city: 'Shanghai', + type: 'Tea', + value: 14000 + } + ] + } + ], + xField: ['city', 'type'], + yField: 'value', + seriesField: 'type', + legends: { + orient: 'right', + position: 'start', + padding: { + left: 12 + }, + item: { + focus: true + }, + defaultSelected: ['Snacks', 'Rice and flour'], + allowAllCanceled: true + } + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + await page.evaluate(() => { + window.__selectedBefore = window.__visualChart.getLegendSelectedDataByIndex(); + window.__legendDataBefore = JSON.stringify( + window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData + ); + }); + const items = await page.evaluate(() => + window.__visualChart + .getStage() + .findAll(g => g.name === 'legendItem', true) + .map(g => { + const b = g.globalAABBBounds; + return { + x: (b.x1 + b.x2) / 2, + y: (b.y1 + b.y2) / 2, + texts: g.findAll(n => n.type === 'text', true).map(n => String(n.attribute.text)) + }; + }) + ); + if (items.length < 2) throw Error('缺少图例项'); + const selected = await page.evaluate(() => window.__selectedBefore); + for (const p of items.filter(p => p.texts.some(t => selected.includes(t)))) await page.mouse.click(p.x, p.y); + await page.waitForFunction( + () => + window.__visualChart.getLegendSelectedDataByIndex().length === 0 && + window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData.length === 0 + ); + await page.evaluate(() => { + window.__clearedAll = true; + }); + await page.mouse.click(items[0].x, items[0].y); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const now = window.__visualChart.getLegendSelectedDataByIndex(); + return ( + window.__selectedBefore && + window.__clearedAll === true && + now.length === 1 && + JSON.stringify(now) !== JSON.stringify(window.__selectedBefore) + ); + }); + await page.evaluate(() => { + const data = JSON.stringify(window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData); + if (data === window.__legendDataBefore) throw Error('图例没有改变实际数据'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-continuous-filter.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-continuous-filter.mjs new file mode 100644 index 0000000000..83037406ea --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-continuous-filter.mjs @@ -0,0 +1,452 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 665d954d46251700ade1e77f + * 验证目的:拖动连续颜色图例后筛选矩形树图数据。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:保留来源动作或按源事件执行操作并检查结果。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'treemap', + + color: { + type: 'linear', + domain: [ + { + dataId: 'data', + fields: ['value'] + } + ], + range: ['#b2e3ff', '#0050b3'] + }, + data: [ + { + id: 'data', + values: [ + { + name: 'query', + children: [ + { + name: 'methods', + children: [ + { name: 'add', value: 593 }, + { name: 'and', value: 330 }, + { name: 'average', value: 287 }, + { name: 'count', value: 277 }, + { name: 'distinct', value: 292 }, + { name: 'div', value: 595 }, + { name: 'eq', value: 594 }, + { name: 'fn', value: 460 }, + { name: 'gt', value: 603 }, + { name: 'gte', value: 625 }, + { name: 'iff', value: 748 }, + { name: 'isa', value: 461 }, + { name: 'lt', value: 597 }, + { name: 'lte', value: 619 }, + { name: 'max', value: 283 }, + { name: 'min', value: 283 }, + { name: 'mod', value: 591 }, + { name: 'mul', value: 603 }, + { name: 'neq', value: 599 }, + { name: 'not', value: 386 }, + { name: 'or', value: 323 }, + { name: 'orderby', value: 307 }, + { name: 'range', value: 772 }, + { name: 'select', value: 296 }, + { name: 'stddev', value: 363 }, + { name: 'sub', value: 600 }, + { name: 'sum', value: 280 }, + { name: 'update', value: 307 }, + { name: 'variance', value: 335 }, + { name: 'where', value: 299 }, + { name: 'xor', value: 354 }, + { name: '_', value: 264 } + ] + }, + { name: 'AggregateExpression', value: 1616 }, + { name: 'And', value: 1027 }, + { name: 'Arithmetic', value: 3891 }, + { name: 'Average', value: 891 }, + { name: 'BinaryExpression', value: 2893 }, + { name: 'Comparison', value: 5103 }, + { name: 'CompositeExpression', value: 3677 }, + { name: 'Count', value: 781 }, + { name: 'DateUtil', value: 4141 }, + { name: 'Distinct', value: 933 }, + { name: 'Expression', value: 5130 }, + { name: 'ExpressionIterator', value: 3617 }, + { name: 'Fn', value: 3240 }, + { name: 'If', value: 2732 }, + { name: 'IsA', value: 2039 }, + { name: 'Literal', value: 1214 }, + { name: 'Match', value: 3748 }, + { name: 'Maximum', value: 843 }, + { name: 'Minimum', value: 843 }, + { name: 'Not', value: 1554 }, + { name: 'Or', value: 970 }, + { name: 'Query', value: 13896 }, + { name: 'Range', value: 1594 }, + { name: 'StringUtil', value: 4130 }, + { name: 'Sum', value: 791 }, + { name: 'Variable', value: 1124 }, + { name: 'Variance', value: 1876 }, + { name: 'Xor', value: 1101 } + ] + }, + { + name: 'util', + children: [ + { + name: 'palette', + children: [ + { name: 'ColorPalette', value: 6367 }, + { name: 'Palette', value: 1229 }, + { name: 'ShapePalette', value: 2059 }, + { name: 'valuePalette', value: 2291 } + ] + }, + { + name: 'math', + children: [ + { name: 'DenseMatrix', value: 3165 }, + { name: 'IMatrix', value: 2815 }, + { name: 'SparseMatrix', value: 3366 } + ] + }, + { + name: 'heap', + children: [ + { name: 'FibonacciHeap', value: 9354 }, + { name: 'HeapNode', value: 1233 } + ] + }, + { name: 'Arrays', value: 8258 }, + { name: 'Colors', value: 10001 }, + { name: 'Dates', value: 8217 }, + { name: 'Displays', value: 12555 }, + { name: 'Filter', value: 2324 }, + { name: 'Geometry', value: 10993 }, + { name: 'IEvaluable', value: 335 }, + { name: 'IPredicate', value: 383 }, + { name: 'IValueProxy', value: 874 }, + { name: 'Maths', value: 17705 }, + { name: 'Orientation', value: 1486 }, + { name: 'Property', value: 5559 }, + { name: 'Shapes', value: 19118 }, + { name: 'Sort', value: 6887 }, + { name: 'Stats', value: 6557 }, + { name: 'Strings', value: 22026 } + ] + }, + { + name: 'animate', + children: [ + { + name: 'interpolate', + children: [ + { name: 'ArrayInterpolator', value: 1983 }, + { name: 'ColorInterpolator', value: 2047 }, + { name: 'DateInterpolator', value: 1375 }, + { name: 'Interpolator', value: 8746 }, + { name: 'MatrixInterpolator', value: 2202 }, + { name: 'NumberInterpolator', value: 1382 }, + { name: 'ObjectInterpolator', value: 1629 }, + { name: 'PointInterpolator', value: 1675 }, + { name: 'RectangleInterpolator', value: 2042 } + ] + }, + { name: 'Easing', value: 17010 }, + { name: 'FunctionSequence', value: 5842 }, + { name: 'ISchedulable', value: 1041 }, + { name: 'Parallel', value: 5176 }, + { name: 'Pause', value: 449 }, + { name: 'Scheduler', value: 5593 }, + { name: 'Sequence', value: 5534 }, + { name: 'Transition', value: 9201 }, + { name: 'Transitioner', value: 19975 }, + { name: 'TransitionEvent', value: 1116 }, + { name: 'Tween', value: 6006 } + ] + }, + { + name: 'scale', + children: [ + { name: 'IScaleMap', value: 2105 }, + { name: 'LinearScale', value: 1316 }, + { name: 'LogScale', value: 3151 }, + { name: 'OrdinalScale', value: 3770 }, + { name: 'QuantileScale', value: 2435 }, + { name: 'QuantitativeScale', value: 4839 }, + { name: 'RootScale', value: 1756 }, + { name: 'Scale', value: 4268 }, + { name: 'ScaleType', value: 1821 }, + { name: 'TimeScale', value: 5833 } + ] + }, + { + name: 'physics', + children: [ + { name: 'DragForce', value: 1082 }, + { name: 'GravityForce', value: 1336 }, + { name: 'IForce', value: 319 }, + { name: 'NBodyForce', value: 10498 }, + { name: 'Particle', value: 2822 }, + { name: 'Simulation', value: 9983 }, + { name: 'Spring', value: 2213 }, + { name: 'SpringForce', value: 1681 } + ] + }, + { + name: 'data', + children: [ + { + name: 'converters', + children: [ + { name: 'Converters', value: 721 }, + { name: 'DelimitedTextConverter', value: 4294 }, + { name: 'GraphMLConverter', value: 9800 }, + { name: 'IDataConverter', value: 1314 }, + { name: 'JSONConverter', value: 2220 } + ] + }, + { name: 'DataField', value: 1759 }, + { name: 'DataSchema', value: 2165 }, + { name: 'DataSet', value: 586 }, + { name: 'DataSource', value: 3331 }, + { name: 'DataTable', value: 772 }, + { name: 'DataUtil', value: 3322 } + ] + }, + { + name: 'vis', + children: [ + { + name: 'controls', + children: [ + { name: 'AnchorControl', value: 2138 }, + { name: 'ClickControl', value: 3824 }, + { name: 'Control', value: 1353 }, + { name: 'ControlList', value: 4665 }, + { name: 'DragControl', value: 2649 }, + { name: 'ExpandControl', value: 2832 }, + { name: 'HoverControl', value: 4896 }, + { name: 'IControl', value: 763 }, + { name: 'PanZoomControl', value: 5222 }, + { name: 'SelectionControl', value: 7862 }, + { name: 'TooltipControl', value: 8435 } + ] + }, + { + name: 'operator', + children: [ + { + name: 'layout', + children: [ + { name: 'AxisLayout', value: 6725 }, + { name: 'BundledEdgeRouter', value: 3727 }, + { name: 'CircleLayout', value: 9317 }, + { name: 'CirclePackingLayout', value: 12003 }, + { name: 'DendrogramLayout', value: 4853 }, + { name: 'ForceDirectedLayout', value: 8411 }, + { name: 'IcicleTreeLayout', value: 4864 }, + { name: 'IndentedTreeLayout', value: 3174 }, + { name: 'Layout', value: 7881 }, + { name: 'NodeLinkTreeLayout', value: 12870 }, + { name: 'PieLayout', value: 2728 }, + { name: 'RadialTreeLayout', value: 12348 }, + { name: 'RandomLayout', value: 870 }, + { name: 'StackedAreaLayout', value: 9121 }, + { name: 'TreeMapLayout', value: 9191 } + ] + }, + { + name: 'encoder', + children: [ + { name: 'ColorEncoder', value: 3179 }, + { name: 'Encoder', value: 4060 }, + { name: 'PropertyEncoder', value: 4138 }, + { name: 'ShapeEncoder', value: 1690 }, + { name: 'valueEncoder', value: 1830 } + ] + }, + { + name: 'distortion', + children: [ + { name: 'BifocalDistortion', value: 4461 }, + { name: 'Distortion', value: 6314 }, + { name: 'FisheyeDistortion', value: 3444 } + ] + }, + { + name: 'filter', + children: [ + { name: 'FisheyeTreeFilter', value: 5219 }, + { name: 'GraphDistanceFilter', value: 3165 }, + { name: 'VisibilityFilter', value: 3509 } + ] + }, + { + name: 'label', + children: [ + { name: 'Labeler', value: 9956 }, + { name: 'RadialLabeler', value: 3899 }, + { name: 'StackedAreaLabeler', value: 3202 } + ] + }, + { name: 'IOperator', value: 1286 }, + { name: 'Operator', value: 2490 }, + { name: 'OperatorList', value: 5248 }, + { name: 'OperatorSequence', value: 4190 }, + { name: 'OperatorSwitch', value: 2581 }, + { name: 'SortOperator', value: 2023 } + ] + }, + { + name: 'data', + children: [ + { + name: 'render', + children: [ + { name: 'ArrowType', value: 698 }, + { name: 'EdgeRenderer', value: 5569 }, + { name: 'IRenderer', value: 353 }, + { name: 'ShapeRenderer', value: 2247 } + ] + }, + { name: 'Data', value: 20544 }, + { name: 'DataList', value: 19788 }, + { name: 'DataSprite', value: 10349 }, + { name: 'EdgeSprite', value: 3301 }, + { name: 'NodeSprite', value: 19382 }, + { name: 'ScaleBinding', value: 11275 }, + { name: 'Tree', value: 7147 }, + { name: 'TreeBuilder', value: 9930 } + ] + }, + { + name: 'axis', + children: [ + { name: 'Axes', value: 1302 }, + { name: 'Axis', value: 24593 }, + { name: 'AxisGridLine', value: 652 }, + { name: 'AxisLabel', value: 636 }, + { name: 'CartesianAxes', value: 6703 } + ] + }, + { + name: 'events', + children: [ + { name: 'DataEvent', value: 2313 }, + { name: 'SelectionEvent', value: 1880 }, + { name: 'TooltipEvent', value: 1701 }, + { name: 'VisualizationEvent', value: 1117 } + ] + }, + { + name: 'legend', + children: [ + { name: 'Legend', value: 20859 }, + { name: 'LegendItem', value: 4614 }, + { name: 'LegendRange', value: 10530 } + ] + }, + { name: 'Visualization', value: 16540 } + ] + }, + { + name: 'display', + children: [ + { name: 'DirtySprite', value: 8833 }, + { name: 'LineSprite', value: 1732 }, + { name: 'RectSprite', value: 3623 }, + { name: 'TextSprite', value: 10066 } + ] + }, + { + name: 'analytics', + children: [ + { + name: 'graph', + children: [ + { name: 'BetweennessCentrality', value: 3534 }, + { name: 'LinkDistance', value: 5731 }, + { name: 'MaxFlowMinCut', value: 7840 }, + { name: 'ShortestPaths', value: 5914 }, + { name: 'SpanningTree', value: 3416 } + ] + }, + { + name: 'cluster', + children: [ + { name: 'AgglomerativeCluster', value: 3938 }, + { name: 'CommunityStructure', value: 3812 }, + { name: 'HierarchicalCluster', value: 6714 }, + { name: 'MergeEdge', value: 743 } + ] + }, + { + name: 'optimization', + children: [{ name: 'AspectRatioBanker', value: 7074 }] + } + ] + }, + { name: 'flex', children: [{ name: 'FlareVis', value: 4116 }] } + ] + } + ], + legends: [ + { + type: 'color', + visible: true, + field: 'value' + } + ], + categoryField: 'name', + valueField: 'value', + seriesField: 'value', + splitType: 'squarify', + aspectRatio: 1, + label: { + visible: true, + style: { + fontSize: 12 + } + }, + title: { + visible: true, + text: 'The software class hierarchy of the Flare visualization toolkit' + } + }; + + return spec; + }, + async exercise(page) { + // 适配源动作;鼠标定位按实际图元,避免绑定旧宿主像素坐标。 + await page.evaluate(() => { + const chart = window.__visualChart; + window.__beforeLegend = chart.getChart().getAllSeries()[0].getSeriesMark().getGraphics().length; + chart.on('legendFilter', e => (window.__legendRange = e.value)); + }); + const point = await graphicCenter(page, 'sliderStartHandler'); + const end = await graphicCenter(page, 'sliderEndHandler'); + await page.mouse.move(point.x, point.y); + await page.mouse.down(); + await page.mouse.move(point.x + (end.x - point.x) * 0.35, point.y + (end.y - point.y) * 0.35, { steps: 10 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + await page.waitForFunction( + () => + Array.isArray(window.__legendRange) && + window.__legendRange[0] > 0 && + window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics().length < window.__beforeLegend + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-custom-value.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-custom-value.mjs new file mode 100644 index 0000000000..ad4c6c3d34 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-custom-value.mjs @@ -0,0 +1,472 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e042f + * 验证目的:自定义图例值与点击后的真实数据筛选。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { date: '2015-01-04', type: 'nlp', value: 8 }, + { date: '2015-01-04', type: 'blockchain', value: 2 }, + { date: '2015-01-11', type: 'nlp', value: 8 }, + { date: '2015-01-11', type: 'blockchain', value: 2 }, + { date: '2015-01-18', type: 'nlp', value: 8 }, + { date: '2015-01-18', type: 'blockchain', value: 3 }, + { date: '2015-01-25', type: 'nlp', value: 8 }, + { date: '2015-01-25', type: 'blockchain', value: 3 }, + { date: '2015-02-01', type: 'nlp', value: 8 }, + { date: '2015-02-01', type: 'blockchain', value: 2 }, + { date: '2015-02-08', type: 'nlp', value: 8 }, + { date: '2015-02-08', type: 'blockchain', value: 2 }, + { date: '2015-02-15', type: 'nlp', value: 8 }, + { date: '2015-02-15', type: 'blockchain', value: 3 }, + { date: '2015-02-22', type: 'nlp', value: 8 }, + { date: '2015-02-22', type: 'blockchain', value: 2 }, + { date: '2015-03-01', type: 'nlp', value: 8 }, + { date: '2015-03-01', type: 'blockchain', value: 2 }, + { date: '2015-03-08', type: 'nlp', value: 8 }, + { date: '2015-03-08', type: 'blockchain', value: 2 }, + { date: '2015-03-15', type: 'nlp', value: 8 }, + { date: '2015-03-15', type: 'blockchain', value: 2 }, + { date: '2015-03-22', type: 'nlp', value: 9 }, + { date: '2015-03-22', type: 'blockchain', value: 2 }, + { date: '2015-03-29', type: 'nlp', value: 8 }, + { date: '2015-03-29', type: 'blockchain', value: 2 }, + { date: '2015-04-05', type: 'nlp', value: 7 }, + { date: '2015-04-05', type: 'blockchain', value: 2 }, + { date: '2015-04-12', type: 'nlp', value: 8 }, + { date: '2015-04-12', type: 'blockchain', value: 2 }, + { date: '2015-04-19', type: 'nlp', value: 8 }, + { date: '2015-04-19', type: 'blockchain', value: 2 }, + { date: '2015-04-26', type: 'nlp', value: 8 }, + { date: '2015-04-26', type: 'blockchain', value: 2 }, + { date: '2015-05-03', type: 'nlp', value: 8 }, + { date: '2015-05-03', type: 'blockchain', value: 2 }, + { date: '2015-05-10', type: 'nlp', value: 7 }, + { date: '2015-05-10', type: 'blockchain', value: 3 }, + { date: '2015-05-17', type: 'nlp', value: 8 }, + { date: '2015-05-17', type: 'blockchain', value: 2 }, + { date: '2015-05-24', type: 'nlp', value: 8 }, + { date: '2015-05-24', type: 'blockchain', value: 3 }, + { date: '2015-05-31', type: 'nlp', value: 8 }, + { date: '2015-05-31', type: 'blockchain', value: 3 }, + { date: '2015-06-07', type: 'nlp', value: 7 }, + { date: '2015-06-07', type: 'blockchain', value: 3 }, + { date: '2015-06-14', type: 'nlp', value: 8 }, + { date: '2015-06-14', type: 'blockchain', value: 3 }, + { date: '2015-06-21', type: 'nlp', value: 8 }, + { date: '2015-06-21', type: 'blockchain', value: 3 }, + { date: '2015-06-28', type: 'nlp', value: 7 }, + { date: '2015-06-28', type: 'blockchain', value: 3 }, + { date: '2015-07-05', type: 'nlp', value: 7 }, + { date: '2015-07-05', type: 'blockchain', value: 4 }, + { date: '2015-07-12', type: 'nlp', value: 7 }, + { date: '2015-07-12', type: 'blockchain', value: 3 }, + { date: '2015-07-19', type: 'nlp', value: 7 }, + { date: '2015-07-19', type: 'blockchain', value: 3 }, + { date: '2015-07-26', type: 'nlp', value: 7 }, + { date: '2015-07-26', type: 'blockchain', value: 3 }, + { date: '2015-08-02', type: 'nlp', value: 7 }, + { date: '2015-08-02', type: 'blockchain', value: 3 }, + { date: '2015-08-09', type: 'nlp', value: 7 }, + { date: '2015-08-09', type: 'blockchain', value: 3 }, + { date: '2015-08-16', type: 'nlp', value: 8 }, + { date: '2015-08-16', type: 'blockchain', value: 3 }, + { date: '2015-08-23', type: 'nlp', value: 8 }, + { date: '2015-08-23', type: 'blockchain', value: 4 }, + { date: '2015-08-30', type: 'nlp', value: 7 }, + { date: '2015-08-30', type: 'blockchain', value: 4 }, + { date: '2015-09-06', type: 'nlp', value: 8 }, + { date: '2015-09-06', type: 'blockchain', value: 4 }, + { date: '2015-09-13', type: 'nlp', value: 7 }, + { date: '2015-09-13', type: 'blockchain', value: 4 }, + { date: '2015-09-20', type: 'nlp', value: 7 }, + { date: '2015-09-20', type: 'blockchain', value: 4 }, + { date: '2015-09-27', type: 'nlp', value: 7 }, + { date: '2015-09-27', type: 'blockchain', value: 4 }, + { date: '2015-10-04', type: 'nlp', value: 8 }, + { date: '2015-10-04', type: 'blockchain', value: 4 }, + { date: '2015-10-11', type: 'nlp', value: 8 }, + { date: '2015-10-11', type: 'blockchain', value: 4 }, + { date: '2015-10-18', type: 'nlp', value: 8 }, + { date: '2015-10-18', type: 'blockchain', value: 5 }, + { date: '2015-10-25', type: 'nlp', value: 8 }, + { date: '2015-10-25', type: 'blockchain', value: 7 }, + { date: '2015-11-01', type: 'nlp', value: 8 }, + { date: '2015-11-01', type: 'blockchain', value: 7 }, + { date: '2015-11-08', type: 'nlp', value: 7 }, + { date: '2015-11-08', type: 'blockchain', value: 6 }, + { date: '2015-11-15', type: 'nlp', value: 8 }, + { date: '2015-11-15', type: 'blockchain', value: 5 }, + { date: '2015-11-22', type: 'nlp', value: 7 }, + { date: '2015-11-22', type: 'blockchain', value: 5 }, + { date: '2015-11-29', type: 'nlp', value: 7 }, + { date: '2015-11-29', type: 'blockchain', value: 6 }, + { date: '2015-12-06', type: 'nlp', value: 7 }, + { date: '2015-12-06', type: 'blockchain', value: 6 }, + { date: '2015-12-13', type: 'nlp', value: 7 }, + { date: '2015-12-13', type: 'blockchain', value: 7 }, + { date: '2015-12-20', type: 'nlp', value: 6 }, + { date: '2015-12-20', type: 'blockchain', value: 6 }, + { date: '2015-12-27', type: 'nlp', value: 6 }, + { date: '2015-12-27', type: 'blockchain', value: 5 }, + { date: '2016-01-03', type: 'nlp', value: 7 }, + { date: '2016-01-03', type: 'blockchain', value: 6 }, + { date: '2016-01-10', type: 'nlp', value: 7 }, + { date: '2016-01-10', type: 'blockchain', value: 6 }, + { date: '2016-01-17', type: 'nlp', value: 8 }, + { date: '2016-01-17', type: 'blockchain', value: 7 }, + { date: '2016-01-24', type: 'nlp', value: 7 }, + { date: '2016-01-24', type: 'blockchain', value: 7 }, + { date: '2016-01-31', type: 'nlp', value: 8 }, + { date: '2016-01-31', type: 'blockchain', value: 7 }, + { date: '2016-02-07', type: 'nlp', value: 8 }, + { date: '2016-02-07', type: 'blockchain', value: 7 }, + { date: '2016-02-14', type: 'nlp', value: 8 }, + { date: '2016-02-14', type: 'blockchain', value: 8 }, + { date: '2016-02-21', type: 'nlp', value: 7 }, + { date: '2016-02-21', type: 'blockchain', value: 7 }, + { date: '2016-02-28', type: 'nlp', value: 8 }, + { date: '2016-02-28', type: 'blockchain', value: 9 }, + { date: '2016-03-06', type: 'nlp', value: 8 }, + { date: '2016-03-06', type: 'blockchain', value: 8 }, + { date: '2016-03-13', type: 'nlp', value: 7 }, + { date: '2016-03-13', type: 'blockchain', value: 7 }, + { date: '2016-03-20', type: 'nlp', value: 7 }, + { date: '2016-03-20', type: 'blockchain', value: 7 }, + { date: '2016-03-27', type: 'nlp', value: 8 }, + { date: '2016-03-27', type: 'blockchain', value: 8 }, + { date: '2016-04-03', type: 'nlp', value: 8 }, + { date: '2016-04-03', type: 'blockchain', value: 8 }, + { date: '2016-04-10', type: 'nlp', value: 8 }, + { date: '2016-04-10', type: 'blockchain', value: 8 }, + { date: '2016-04-17', type: 'nlp', value: 7 }, + { date: '2016-04-17', type: 'blockchain', value: 8 }, + { date: '2016-04-24', type: 'nlp', value: 8 }, + { date: '2016-04-24', type: 'blockchain', value: 9 }, + { date: '2016-05-01', type: 'nlp', value: 7 }, + { date: '2016-05-01', type: 'blockchain', value: 10 }, + { date: '2016-05-08', type: 'nlp', value: 8 }, + { date: '2016-05-08', type: 'blockchain', value: 10 }, + { date: '2016-05-15', type: 'nlp', value: 10 }, + { date: '2016-05-15', type: 'blockchain', value: 10 }, + { date: '2016-05-22', type: 'nlp', value: 8 }, + { date: '2016-05-22', type: 'blockchain', value: 10 }, + { date: '2016-05-29', type: 'nlp', value: 7 }, + { date: '2016-05-29', type: 'blockchain', value: 10 }, + { date: '2016-06-05', type: 'nlp', value: 8 }, + { date: '2016-06-05', type: 'blockchain', value: 10 }, + { date: '2016-06-12', type: 'nlp', value: 8 }, + { date: '2016-06-12', type: 'blockchain', value: 11 }, + { date: '2016-06-19', type: 'nlp', value: 7 }, + { date: '2016-06-19', type: 'blockchain', value: 11 }, + { date: '2016-06-26', type: 'nlp', value: 7 }, + { date: '2016-06-26', type: 'blockchain', value: 10 }, + { date: '2016-07-03', type: 'nlp', value: 7 }, + { date: '2016-07-03', type: 'blockchain', value: 10 }, + { date: '2016-07-10', type: 'nlp', value: 7 }, + { date: '2016-07-10', type: 'blockchain', value: 10 }, + { date: '2016-07-17', type: 'nlp', value: 7 }, + { date: '2016-07-17', type: 'blockchain', value: 9 }, + { date: '2016-07-24', type: 'nlp', value: 7 }, + { date: '2016-07-24', type: 'blockchain', value: 9 }, + { date: '2016-07-31', type: 'nlp', value: 7 }, + { date: '2016-07-31', type: 'blockchain', value: 9 }, + { date: '2016-08-07', type: 'nlp', value: 6 }, + { date: '2016-08-07', type: 'blockchain', value: 8 }, + { date: '2016-08-14', type: 'nlp', value: 7 }, + { date: '2016-08-14', type: 'blockchain', value: 9 }, + { date: '2016-08-21', type: 'nlp', value: 7 }, + { date: '2016-08-21', type: 'blockchain', value: 10 }, + { date: '2016-08-28', type: 'nlp', value: 7 }, + { date: '2016-08-28', type: 'blockchain', value: 11 }, + { date: '2016-09-04', type: 'nlp', value: 7 }, + { date: '2016-09-04', type: 'blockchain', value: 11 }, + { date: '2016-09-11', type: 'nlp', value: 8 }, + { date: '2016-09-11', type: 'blockchain', value: 10 }, + { date: '2016-09-18', type: 'nlp', value: 7 }, + { date: '2016-09-18', type: 'blockchain', value: 11 }, + { date: '2016-09-25', type: 'nlp', value: 7 }, + { date: '2016-09-25', type: 'blockchain', value: 11 }, + { date: '2016-10-02', type: 'nlp', value: 7 }, + { date: '2016-10-02', type: 'blockchain', value: 11 }, + { date: '2016-10-09', type: 'nlp', value: 8 }, + { date: '2016-10-09', type: 'blockchain', value: 14 }, + { date: '2016-10-16', type: 'nlp', value: 8 }, + { date: '2016-10-16', type: 'blockchain', value: 12 }, + { date: '2016-10-23', type: 'nlp', value: 7 }, + { date: '2016-10-23', type: 'blockchain', value: 13 }, + { date: '2016-10-30', type: 'nlp', value: 7 }, + { date: '2016-10-30', type: 'blockchain', value: 13 }, + { date: '2016-11-06', type: 'nlp', value: 8 }, + { date: '2016-11-06', type: 'blockchain', value: 13 }, + { date: '2016-11-13', type: 'nlp', value: 8 }, + { date: '2016-11-13', type: 'blockchain', value: 15 }, + { date: '2016-11-20', type: 'nlp', value: 8 }, + { date: '2016-11-20', type: 'blockchain', value: 14 }, + { date: '2016-11-27', type: 'nlp', value: 8 }, + { date: '2016-11-27', type: 'blockchain', value: 14 }, + { date: '2016-12-04', type: 'nlp', value: 8 }, + { date: '2016-12-04', type: 'blockchain', value: 14 }, + { date: '2016-12-11', type: 'nlp', value: 8 }, + { date: '2016-12-11', type: 'blockchain', value: 13 }, + { date: '2016-12-18', type: 'nlp', value: 7 }, + { date: '2016-12-18', type: 'blockchain', value: 13 }, + { date: '2016-12-25', type: 'nlp', value: 6 }, + { date: '2016-12-25', type: 'blockchain', value: 10 }, + { date: '2017-01-01', type: 'nlp', value: 7 }, + { date: '2017-01-01', type: 'blockchain', value: 14 }, + { date: '2017-01-08', type: 'nlp', value: 8 }, + { date: '2017-01-08', type: 'blockchain', value: 15 }, + { date: '2017-01-15', type: 'nlp', value: 8 }, + { date: '2017-01-15', type: 'blockchain', value: 14 }, + { date: '2017-01-22', type: 'nlp', value: 7 }, + { date: '2017-01-22', type: 'blockchain', value: 15 }, + { date: '2017-01-29', type: 'nlp', value: 8 }, + { date: '2017-01-29', type: 'blockchain', value: 14 }, + { date: '2017-02-05', type: 'nlp', value: 8 }, + { date: '2017-02-05', type: 'blockchain', value: 15 }, + { date: '2017-02-12', type: 'nlp', value: 8 }, + { date: '2017-02-12', type: 'blockchain', value: 15 }, + { date: '2017-02-19', type: 'nlp', value: 8 }, + { date: '2017-02-19', type: 'blockchain', value: 17 }, + { date: '2017-02-26', type: 'nlp', value: 8 }, + { date: '2017-02-26', type: 'blockchain', value: 18 }, + { date: '2017-03-05', type: 'nlp', value: 9 }, + { date: '2017-03-05', type: 'blockchain', value: 18 }, + { date: '2017-03-12', type: 'nlp', value: 8 }, + { date: '2017-03-12', type: 'blockchain', value: 17 }, + { date: '2017-03-19', type: 'nlp', value: 8 }, + { date: '2017-03-19', type: 'blockchain', value: 19 }, + { date: '2017-03-26', type: 'nlp', value: 8 }, + { date: '2017-03-26', type: 'blockchain', value: 17 }, + { date: '2017-04-02', type: 'nlp', value: 8 }, + { date: '2017-04-02', type: 'blockchain', value: 17 }, + { date: '2017-04-09', type: 'nlp', value: 8 }, + { date: '2017-04-09', type: 'blockchain', value: 16 }, + { date: '2017-04-16', type: 'nlp', value: 8 }, + { date: '2017-04-16', type: 'blockchain', value: 17 }, + { date: '2017-04-23', type: 'nlp', value: 8 }, + { date: '2017-04-23', type: 'blockchain', value: 18 }, + { date: '2017-04-30', type: 'nlp', value: 8 }, + { date: '2017-04-30', type: 'blockchain', value: 20 }, + { date: '2017-05-07', type: 'nlp', value: 8 }, + { date: '2017-05-07', type: 'blockchain', value: 23 }, + { date: '2017-05-14', type: 'nlp', value: 8 }, + { date: '2017-05-14', type: 'blockchain', value: 24 }, + { date: '2017-05-21', type: 'nlp', value: 8 }, + { date: '2017-05-21', type: 'blockchain', value: 30 }, + { date: '2017-05-28', type: 'nlp', value: 8 }, + { date: '2017-05-28', type: 'blockchain', value: 26 }, + { date: '2017-06-04', type: 'nlp', value: 8 }, + { date: '2017-06-04', type: 'blockchain', value: 29 }, + { date: '2017-06-11', type: 'nlp', value: 8 }, + { date: '2017-06-11', type: 'blockchain', value: 30 }, + { date: '2017-06-18', type: 'nlp', value: 8 }, + { date: '2017-06-18', type: 'blockchain', value: 29 }, + { date: '2017-06-25', type: 'nlp', value: 8 }, + { date: '2017-06-25', type: 'blockchain', value: 29 }, + { date: '2017-07-02', type: 'nlp', value: 7 }, + { date: '2017-07-02', type: 'blockchain', value: 24 }, + { date: '2017-07-09', type: 'nlp', value: 8 }, + { date: '2017-07-09', type: 'blockchain', value: 26 }, + { date: '2017-07-16', type: 'nlp', value: 8 }, + { date: '2017-07-16', type: 'blockchain', value: 26 }, + { date: '2017-07-23', type: 'nlp', value: 8 }, + { date: '2017-07-23', type: 'blockchain', value: 26 }, + { date: '2017-07-30', type: 'nlp', value: 8 }, + { date: '2017-07-30', type: 'blockchain', value: 27 }, + { date: '2017-08-06', type: 'nlp', value: 8 }, + { date: '2017-08-06', type: 'blockchain', value: 28 }, + { date: '2017-08-13', type: 'nlp', value: 8 }, + { date: '2017-08-13', type: 'blockchain', value: 32 }, + { date: '2017-08-20', type: 'nlp', value: 8 }, + { date: '2017-08-20', type: 'blockchain', value: 33 }, + { date: '2017-08-27', type: 'nlp', value: 7 }, + { date: '2017-08-27', type: 'blockchain', value: 34 }, + { date: '2017-09-03', type: 'nlp', value: 7 }, + { date: '2017-09-03', type: 'blockchain', value: 34 }, + { date: '2017-09-10', type: 'nlp', value: 8 }, + { date: '2017-09-10', type: 'blockchain', value: 36 }, + { date: '2017-09-17', type: 'nlp', value: 8 }, + { date: '2017-09-17', type: 'blockchain', value: 34 }, + { date: '2017-09-24', type: 'nlp', value: 8 }, + { date: '2017-09-24', type: 'blockchain', value: 34 }, + { date: '2017-10-01', type: 'nlp', value: 8 }, + { date: '2017-10-01', type: 'blockchain', value: 35 }, + { date: '2017-10-08', type: 'nlp', value: 9 }, + { date: '2017-10-08', type: 'blockchain', value: 40 }, + { date: '2017-10-15', type: 'nlp', value: 8 }, + { date: '2017-10-15', type: 'blockchain', value: 45 }, + { date: '2017-10-22', type: 'nlp', value: 9 }, + { date: '2017-10-22', type: 'blockchain', value: 46 }, + { date: '2017-10-29', type: 'nlp', value: 9 }, + { date: '2017-10-29', type: 'blockchain', value: 49 }, + { date: '2017-11-05', type: 'nlp', value: 9 }, + { date: '2017-11-05', type: 'blockchain', value: 52 }, + { date: '2017-11-12', type: 'nlp', value: 9 }, + { date: '2017-11-12', type: 'blockchain', value: 51 }, + { date: '2017-11-19', type: 'nlp', value: 8 }, + { date: '2017-11-19', type: 'blockchain', value: 52 }, + { date: '2017-11-26', type: 'nlp', value: 8 }, + { date: '2017-11-26', type: 'blockchain', value: 73 }, + { date: '2017-12-03', type: 'nlp', value: 8 }, + { date: '2017-12-03', type: 'blockchain', value: 86 }, + { date: '2017-12-10', type: 'nlp', value: 8 }, + { date: '2017-12-10', type: 'blockchain', value: 91 }, + { date: '2017-12-17', type: 'nlp', value: 7 }, + { date: '2017-12-17', type: 'blockchain', value: 100 }, + { date: '2017-12-24', type: 'nlp', value: 6 }, + { date: '2017-12-24', type: 'blockchain', value: 71 }, + { date: '2017-12-31', type: 'nlp', value: 7 }, + { date: '2017-12-31', type: 'blockchain', value: 77 }, + { date: '2018-01-07', type: 'nlp', value: 8 }, + { date: '2018-01-07', type: 'blockchain', value: 88 }, + { date: '2018-01-14', type: 'nlp', value: 8 }, + { date: '2018-01-14', type: 'blockchain', value: 78 }, + { date: '2018-01-21', type: 'nlp', value: 8 }, + { date: '2018-01-21', type: 'blockchain', value: 70 }, + { date: '2018-01-28', type: 'nlp', value: 11 }, + { date: '2018-01-28', type: 'blockchain', value: 62 }, + { date: '2018-02-04', type: 'nlp', value: 9 }, + { date: '2018-02-04', type: 'blockchain', value: 57 }, + { date: '2018-02-11', type: 'nlp', value: 8 }, + { date: '2018-02-11', type: 'blockchain', value: 54 }, + { date: '2018-02-18', type: 'nlp', value: 8 }, + { date: '2018-02-18', type: 'blockchain', value: 56 }, + { date: '2018-02-25', type: 'nlp', value: 8 }, + { date: '2018-02-25', type: 'blockchain', value: 52 }, + { date: '2018-03-04', type: 'nlp', value: 9 }, + { date: '2018-03-04', type: 'blockchain', value: 50 }, + { date: '2018-03-11', type: 'nlp', value: 9 }, + { date: '2018-03-11', type: 'blockchain', value: 51 }, + { date: '2018-03-18', type: 'nlp', value: 9 }, + { date: '2018-03-18', type: 'blockchain', value: 50 }, + { date: '2018-03-25', type: 'nlp', value: 8 }, + { date: '2018-03-25', type: 'blockchain', value: 43 }, + { date: '2018-04-01', type: 'nlp', value: 8 }, + { date: '2018-04-01', type: 'blockchain', value: 42 }, + { date: '2018-04-08', type: 'nlp', value: 9 }, + { date: '2018-04-08', type: 'blockchain', value: 44 }, + { date: '2018-04-15', type: 'nlp', value: 9 }, + { date: '2018-04-15', type: 'blockchain', value: 46 }, + { date: '2018-04-22', type: 'nlp', value: 9 }, + { date: '2018-04-22', type: 'blockchain', value: 46 }, + { date: '2018-04-29', type: 'nlp', value: 8 }, + { date: '2018-04-29', type: 'blockchain', value: 45 }, + { date: '2018-05-06', type: 'nlp', value: 8 }, + { date: '2018-05-06', type: 'blockchain', value: 45 }, + { date: '2018-05-13', type: 'nlp', value: 9 }, + { date: '2018-05-13', type: 'blockchain', value: 50 }, + { date: '2018-05-20', type: 'nlp', value: 9 }, + { date: '2018-05-20', type: 'blockchain', value: 43 }, + { date: '2018-05-27', type: 'nlp', value: 8 }, + { date: '2018-05-27', type: 'blockchain', value: 43 }, + { date: '2018-06-03', type: 'nlp', value: 9 }, + { date: '2018-06-03', type: 'blockchain', value: 43 }, + { date: '2018-06-10', type: 'nlp', value: 8 }, + { date: '2018-06-10', type: 'blockchain', value: 40 }, + { date: '2018-06-17', type: 'nlp', value: 8 }, + { date: '2018-06-17', type: 'blockchain', value: 41 }, + { date: '2018-06-24', type: 'nlp', value: 8 }, + { date: '2018-06-24', type: 'blockchain', value: 37 }, + { date: '2018-07-01', type: 'nlp', value: 8 }, + { date: '2018-07-01', type: 'blockchain', value: 36 }, + { date: '2018-07-08', type: 'nlp', value: 8 }, + { date: '2018-07-08', type: 'blockchain', value: 37 }, + { date: '2018-07-15', type: 'nlp', value: 8 }, + { date: '2018-07-15', type: 'blockchain', value: 39 }, + { date: '2018-07-22', type: 'nlp', value: 8 }, + { date: '2018-07-22', type: 'blockchain', value: 39 }, + { date: '2018-07-29', type: 'nlp', value: 8 }, + { date: '2018-07-29', type: 'blockchain', value: 37 }, + { date: '2018-08-05', type: 'nlp', value: 8 }, + { date: '2018-08-05', type: 'blockchain', value: 37 }, + { date: '2018-08-12', type: 'nlp', value: 8 }, + { date: '2018-08-12', type: 'blockchain', value: 37 }, + { date: '2018-08-19', type: 'nlp', value: 8 }, + { date: '2018-08-19', type: 'blockchain', value: 37 } + ]; + const spec = { + type: 'line', + data: [{ id: 'line', values: data }], + xField: 'date', + yField: 'value', + seriesField: 'type', + color: ['#1890ff', '#2fc25b'], + point: { + style: { size: 0, lineWidth: 1, stroke: '#fff' }, + state: { dimension_hover: { size: 8, lineWidth: 1, stroke: '#fff' } } + }, + line: { style: { lineWidth: 2 } }, + axes: [ + { orient: 'bottom' }, + { + orient: 'left', + domainLine: { visible: false }, + tick: { visible: false, tickCount: 5 }, + grid: { style: { lineDash: [0] } } + } + ], + legends: { + padding: { top: 12 }, + orient: 'bottom', + autoPage: false, + title: { visible: true, text: 'Total', align: 'end', space: 4, textStyle: { fill: '#1890ff', fontSize: 14 } }, + item: { + width: '100%', + shape: { style: { size: 8, symbolType: 'square' } }, + value: { alignRight: true, style: { fill: '#333' }, state: { unselected: { fill: '#d8d8d8' } } } + }, + data: items => { + return items.map((item, index) => { + item.value = index === 0 ? '20,000' : '7,000'; + return item; + }); + } + }, + crosshair: { + xField: { visible: true, line: { type: 'line' }, label: { visible: false } }, + yField: { visible: false } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate( + () => + (window.__legendBefore = window.__visualChart + .getChart() + .getAllSeries() + .flatMap(s => s.getViewData().latestData).length) + ); + const p = await graphicCenter(page, 'legendItem'); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart; + if (!Number.isFinite(window.__legendBefore)) throw Error('缺少图例筛选前的数据数量'); + if ( + c + .getChart() + .getAllSeries() + .flatMap(s => s.getViewData().latestData).length >= window.__legendBefore + ) + throw Error('图例点击未筛选数据'); + const values = c.getStage().findAll(g => g.name === 'legendItemValue' && g.attribute.visible !== false, true); + if (!values.length) throw Error('自定义图例值未显示'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-filter.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-filter.mjs new file mode 100644 index 0000000000..1e201a1848 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-filter.mjs @@ -0,0 +1,33 @@ +import { barSpec, graphicCenter } from '../../../helpers.mjs'; + +/** + * 验证目的:图例点击筛选(multiple-legend-layout)。 + * 图表类型:bar。 + * 关键配置:legends.orient、legends.position、seriesField。 + * 场景条件:固定两组数据和两个离散图例项。 + * 最终检查:点击真实图例,验证选中项减少一个且可视数据变为三条。 + * 覆盖边界:不验证连续图例、多图例或自定义图例项。 + */ +export default { + createSpec() { + // 使用固定顺序的两个图例项。 + return { ...barSpec(), legends: { visible: true, orient: 'bottom', position: 'start' } }; + }, + async exercise(page) { + // 点击真实图例,并移出鼠标以稳定最终截图。 + await page.evaluate(() => { + window.__legendBefore = window.__visualChart.getLegendSelectedDataByIndex().length; + }); + const point = await graphicCenter(page, 'legendItem'); + await page.mouse.click(point.x, point.y); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 筛选必须同时改变选中项和实际数据。 + await page.waitForFunction( + () => + window.__visualChart.getLegendSelectedDataByIndex().length === window.__legendBefore - 1 && + window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData.length === 3 + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-no-filter.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-no-filter.mjs new file mode 100644 index 0000000000..a40bb2aec5 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-no-filter.mjs @@ -0,0 +1,106 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7f0752a1e9eec95f9ec7 + * 验证目的:图例悬停反向状态、移出恢复。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: [ + { + id: 'bar', + values: [ + { year: '2012', type: 'Forest', value: 320 }, + { year: '2012', type: 'Steppe', value: 220 }, + { year: '2012', type: 'Desert', value: 150 }, + { year: '2012', type: 'Wetland', value: 98 }, + { year: '2013', type: 'Forest', value: 332 }, + { year: '2013', type: 'Steppe', value: 182 }, + { year: '2013', type: 'Desert', value: 232 }, + { year: '2013', type: 'Wetland', value: 77 }, + { year: '2014', type: 'Forest', value: 301 }, + { year: '2014', type: 'Steppe', value: 191 }, + { year: '2014', type: 'Desert', value: 201 }, + { year: '2014', type: 'Wetland', value: 101 }, + { year: '2015', type: 'Forest', value: 334 }, + { year: '2015', type: 'Steppe', value: 234 }, + { year: '2015', type: 'Desert', value: 154 }, + { year: '2015', type: 'Wetland', value: 99 }, + { year: '2016', type: 'Forest', value: 390 }, + { year: '2016', type: 'Steppe', value: 290 }, + { year: '2016', type: 'Desert', value: 190 }, + { year: '2016', type: 'Wetland', value: 40 } + ] + } + ], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + stateDef: { + legend_hover: { + filter: datum => { + return true; + } + } + }, + legends: [ + { + orient: 'top', + position: 'middle', + select: false, + data: items => { + return items.map(item => { + item.shape.outerBorder = { stroke: item.shape.fill, distance: 2, lineWidth: 1 }; + return item; + }); + }, + item: { shape: { space: 8 }, background: { visible: false } } + } + ], + bar: { state: { legend_hover_reverse: { fill: '#ccc' } } } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.on('legendItemHover', e => { + const hoveredName = e?.value?.data?.label; + if (hoveredName) { + chartSpace.updateState({ legend_hover_reverse: { filter: d => d.type !== hoveredName } }); + } + }); + chartSpace.on('legendItemUnHover', e => { + chartSpace.updateState({ legend_hover_reverse: { filter: d => false } }); + }); + }); + const p = await graphicCenter(page, 'legendItem'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + const hover = await seriesStates(page); + await page.mouse.move(950, 750); + await interactionFrame(page); + const off = await seriesStates(page); + + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(v => (window.__legendStates = v), { hover, off }); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__legendStates; + if (!r?.hover.some(m => m.states.legend_hover_reverse > 0) || r.off.some(m => m.states.legend_hover_reverse > 0)) + throw Error('图例悬停与恢复没有生效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-state.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-state.mjs new file mode 100644 index 0000000000..ef4f9dd3c8 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-hover-state.mjs @@ -0,0 +1,121 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame, seriesStates } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e042e + * 验证目的:图例悬停反向状态、移出恢复与单选筛选。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { year: '2012', type: 'Forest', value: 320 }, + { year: '2012', type: 'Steppe', value: 220 }, + { year: '2012', type: 'Desert', value: 150 }, + { year: '2012', type: 'Wetland', value: 98 }, + { year: '2013', type: 'Forest', value: 332 }, + { year: '2013', type: 'Steppe', value: 182 }, + { year: '2013', type: 'Desert', value: 232 }, + { year: '2013', type: 'Wetland', value: 77 }, + { year: '2014', type: 'Forest', value: 301 }, + { year: '2014', type: 'Steppe', value: 191 }, + { year: '2014', type: 'Desert', value: 201 }, + { year: '2014', type: 'Wetland', value: 101 }, + { year: '2015', type: 'Forest', value: 334 }, + { year: '2015', type: 'Steppe', value: 234 }, + { year: '2015', type: 'Desert', value: 154 }, + { year: '2015', type: 'Wetland', value: 99 }, + { year: '2016', type: 'Forest', value: 390 }, + { year: '2016', type: 'Steppe', value: 290 }, + { year: '2016', type: 'Desert', value: 190 }, + { year: '2016', type: 'Wetland', value: 40 } + ]; + const spec = { + type: 'bar', + data: [{ id: 'bar', values: data }], + xField: ['year', 'type'], + yField: 'value', + seriesField: 'type', + stateDef: { + legend_hover: { + filter: datum => { + return true; + } + } + }, + legends: [ + { + orient: 'top', + position: 'middle', + padding: { bottom: 12 }, + data: items => { + return items.map(item => { + item.shape.outerBorder = { stroke: item.shape.fill, distance: 2, lineWidth: 1 }; + return item; + }); + }, + item: { + shape: { space: 8, style: { symbolType: 'square' }, state: { unSelected: { opacity: 0.5 } } }, + background: { visible: false } + }, + selectMode: 'single' + } + ], + crosshair: { xField: { visible: true, label: { visible: false } }, yField: { visible: false } }, + bar: { state: { legend_hover_reverse: { fill: '#ccc' } } } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.on('legendItemHover', e => { + const hoveredName = e?.value?.data?.label; + if (hoveredName) { + chartSpace.updateState({ legend_hover_reverse: { filter: d => d.type !== hoveredName } }); + } + }); + chartSpace.on('legendItemUnHover', e => { + chartSpace.updateState({ legend_hover_reverse: { filter: d => false } }); + }); + }); + const p = await graphicCenter(page, 'legendItem'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + const hover = await seriesStates(page); + await page.mouse.move(950, 750); + await interactionFrame(page); + const off = await seriesStates(page); + await page.evaluate(() => { + window.__legendBefore = window.__visualChart + .getChart() + .getAllSeries() + .flatMap(s => s.getViewData().latestData).length; + }); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(v => (window.__legendStates = v), { hover, off }); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__legendStates; + if (!r?.hover.some(m => m.states.legend_hover_reverse > 0) || r.off.some(m => m.states.legend_hover_reverse > 0)) + throw Error('图例悬停与恢复没有生效'); + if ( + window.__visualChart + .getChart() + .getAllSeries() + .flatMap(s => s.getViewData().latestData).length >= window.__legendBefore + ) + throw Error('图例未过滤实际数据'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-multiple.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-multiple.mjs new file mode 100644 index 0000000000..b79ebe96aa --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-multiple.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0366 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:五个图例在四个方向同时布局。 + * 保留条件:两个左侧及上右下图例、padding=30、line 系列名称。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'line', + padding: 30, + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + series: [ + { + type: 'line', + name: '77' + } + ], + legends: [ + { + visible: true, + orient: 'left' + }, + { + visible: true, + orient: 'left' + }, + { + visible: true, + orient: 'top' + }, + { + visible: true, + orient: 'right' + }, + { + visible: true, + orient: 'bottom' + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-single-selection.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-single-selection.mjs new file mode 100644 index 0000000000..4c76385d1c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-single-selection.mjs @@ -0,0 +1,293 @@ +import { verifySpec } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f0952a1e9eec95f9ecd + * 验证目的:单选图例切换与数据过滤。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'line', + data: [ + { + id: 'line', + values: [ + { + name: 'Type A', + value: 33934, + year: 2010 + }, + { + name: 'Type A', + value: 52503, + year: 2011 + }, + { + name: 'Type A', + value: 57177, + year: 2012 + }, + { + name: 'Type A', + value: 69658, + year: 2013 + }, + { + name: 'Type A', + value: 97031, + year: 2014 + }, + { + name: 'Type A', + value: 119931, + year: 2015 + }, + { + name: 'Type A', + value: 137133, + year: 2016 + }, + { + name: 'Type A', + value: 154175, + year: 2017 + }, + { + name: 'Type B', + value: 24916, + year: 2010 + }, + { + name: 'Type B', + value: 24064, + year: 2011 + }, + { + name: 'Type B', + value: 29742, + year: 2012 + }, + { + name: 'Type B', + value: 29851, + year: 2013 + }, + { + name: 'Type B', + value: 32490, + year: 2014 + }, + { + name: 'Type B', + value: 30282, + year: 2015 + }, + { + name: 'Type B', + value: 38121, + year: 2016 + }, + { + name: 'Type B', + value: 40434, + year: 2017 + }, + { + name: 'Type C', + value: 11744, + year: 2010 + }, + { + name: 'Type C', + value: 17722, + year: 2011 + }, + { + name: 'Type C', + value: 16005, + year: 2012 + }, + { + name: 'Type C', + value: 19771, + year: 2013 + }, + { + name: 'Type C', + value: 20185, + year: 2014 + }, + { + name: 'Type C', + value: 24377, + year: 2015 + }, + { + name: 'Type C', + value: 32147, + year: 2016 + }, + { + name: 'Type C', + value: 39389, + year: 2017 + }, + { + name: 'Type D', + value: null, + year: 2010 + }, + { + name: 'Type D', + value: null, + year: 2011 + }, + { + name: 'Type D', + value: 7988, + year: 2012 + }, + { + name: 'Type D', + value: 12169, + year: 2013 + }, + { + name: 'Type D', + value: 15112, + year: 2014 + }, + { + name: 'Type D', + value: 22452, + year: 2015 + }, + { + name: 'Type D', + value: 34400, + year: 2016 + }, + { + name: 'Type D', + value: 34227, + year: 2017 + }, + { + name: 'Other', + value: 12908, + year: 2010 + }, + { + name: 'Other', + value: 5948, + year: 2011 + }, + { + name: 'Other', + value: 8105, + year: 2012 + }, + { + name: 'Other', + value: 11248, + year: 2013 + }, + { + name: 'Other', + value: 8989, + year: 2014 + }, + { + name: 'Other', + value: 11816, + year: 2015 + }, + { + name: 'Other', + value: 18274, + year: 2016 + }, + { + name: 'Other', + value: 18111, + year: 2017 + } + ] + } + ], + xField: 'year', + yField: 'value', + seriesField: 'name', + legends: { + orient: 'right', + selectMode: 'single', + defaultSelected: ['Type D'], + title: { + visible: true, + text: 'Single selection' + } + }, + axes: [ + { + orient: 'left', + label: { + inside: true, + space: 2, + style: { + textBaseline: 'bottom', + textAlign: 'start', + fontWeight: 'bold' + } + }, + tick: { + visible: false + }, + domainLine: { + visible: false + }, + title: { + visible: true, + text: 'Title' + } + } + ] + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + await page.evaluate(() => { + window.__selectedBefore = window.__visualChart.getLegendSelectedDataByIndex(); + window.__legendDataBefore = JSON.stringify( + window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData + ); + }); + const items = await page.evaluate(() => + window.__visualChart + .getStage() + .findAll(g => g.name === 'legendItem', true) + .map(g => { + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }) + ); + if (items.length < 2) throw Error('缺少图例项'); + await page.mouse.click(items[1].x, items[1].y); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const now = window.__visualChart.getLegendSelectedDataByIndex(); + return ( + window.__selectedBefore && now.length === 1 && JSON.stringify(now) !== JSON.stringify(window.__selectedBefore) + ); + }); + await page.evaluate(() => { + const data = JSON.stringify(window.__visualChart.getChart().getAllSeries()[0].getViewData().latestData); + if (data === window.__legendDataBefore) throw Error('图例没有改变实际数据'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/legend/legend-symbol-hidden.mjs b/packages/vchart/__tests__/visual/cases/components/legend/legend-symbol-hidden.mjs new file mode 100644 index 0000000000..db749baf56 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/legend/legend-symbol-hidden.mjs @@ -0,0 +1,75 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65572371f022be008d9a3fcb + * 验证目的:图例项隐藏符号后的文本布局。 + * 保留条件:xField, yField, legends;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value', + legends: { + visible: true, + item: { + shape: { + visible: false, + style: { + fill: 'red' + } + } + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-area-multiple.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-area-multiple.mjs new file mode 100644 index 0000000000..0f0c103dcf --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-area-multiple.mjs @@ -0,0 +1,88 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0d52a1e9eec95f9eda + * 验证目的:多块标注区域与折线坐标映射。 + * 保留条件:width, xField, yField, markArea;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + width: 800, + type: 'line', + xField: 'x', + yField: 'y', + markArea: [ + { + x: 'min', + x1: 4, + label: { + text: 'Area: minX to x = 4', + position: 'insideTop' + } + }, + { + y: 20, + y1: 40, + label: { + text: 'Area: y = 20 to y = 40', + position: 'insideRight' + } + }, + { + coordinates: [ + { + x: 1, + y: 10 + }, + { + x: 2, + y: 80 + }, + { + x: 3, + y: 80 + }, + { + x: 4, + y: 50 + } + ], + label: { + text: 'Area: data coordinates', + position: 'middle' + } + } + ], + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-area'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-area-scatter-label.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-area-scatter-label.mjs new file mode 100644 index 0000000000..133c28f735 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-area-scatter-label.mjs @@ -0,0 +1,315 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64fadcfee8a02e06c62a07a4 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:隐藏坐标轴时散点标签与三块坐标标注共存。 + * 保留条件:30 条原始数据、三组坐标、透明外框及半透明分区、标签。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'scatter', + xField: 'x', + yField: 'y', + label: { + visible: true, + overlap: { + avoidBaseMark: false, + overlapPadding: 10 + }, + style: { + fill: '#222' + } + }, + axes: [ + { + orient: 'bottom', + type: 'linear', + range: { + min: 140, + max: 220 + }, + visible: false + }, + { + orient: 'left', + visible: false + } + ], + markArea: [ + { + coordinates: [ + { + x: 140, + y: 0 + }, + { + x: 220, + y: 0 + }, + { + x: 220, + y: 80 + }, + { + x: 140, + y: 80 + } + ], + area: { + style: { + fill: '#5B8FF9', + fillOpacity: 0, + stroke: '#5B8FF9' + } + } + }, + { + coordinates: [ + { + x: 140, + y: 0 + }, + { + x: 180, + y: 0 + }, + { + x: 180, + y: 40 + }, + { + x: 140, + y: 40 + } + ], + area: { + style: { + fill: '#5B8FF9', + fillOpacity: 0.15 + } + } + }, + { + coordinates: [ + { + x: 180, + y: 40 + }, + { + x: 220, + y: 40 + }, + { + x: 220, + y: 80 + }, + { + x: 180, + y: 80 + } + ], + area: { + style: { + fill: '#5B8FF9', + fillOpacity: 0.15 + } + } + } + ], + data: { + id: 'data2', + values: [ + { + name: 'Denmark', + x: 201.53, + y: 26.84 + }, + { + name: 'Switzerland', + x: 196.44, + y: 21.73 + }, + { + name: 'Australia', + x: 196.4, + y: 24.09 + }, + { + name: 'New Zealand', + x: 196.09, + y: 19.43, + label: { + anchor: 'left' + } + }, + { + name: 'Germany', + x: 189.87, + y: 27.68 + }, + { + name: 'Austria', + x: 187, + y: 25.43 + }, + { + name: 'Netherlands', + x: 186.46, + y: 29.08, + label: { + anchor: 'left' + } + }, + { + name: 'Spain', + x: 184.69, + y: 40.37, + label: { + anchor: 'left' + } + }, + { + name: 'Finland', + x: 183.98, + y: 14.57 + }, + { + name: 'United States', + x: 181.91, + y: 32.73, + label: { + anchor: 'left' + } + }, + { + name: 'Portugal', + x: 180.66, + y: 29.87 + }, + { + name: 'Sweden', + x: 177.93, + y: 16.59 + }, + { + name: 'United Kingdom', + x: 177.73, + y: 34.24 + }, + { + name: 'Norway', + x: 176.23, + y: 19.28, + label: { + anchor: 'left' + } + }, + { + name: 'Canada', + x: 172.83, + y: 28.17, + label: { + anchor: 'left' + } + }, + { + name: 'Japan', + x: 172.72, + y: 40.9 + }, + { + name: 'France', + x: 172.3, + y: 42.04, + label: { + anchor: 'left' + } + }, + { + name: 'Estonia', + x: 171.71, + y: 19.19 + }, + { + name: 'Ireland', + x: 170.83, + y: 27.47 + }, + { + name: 'Czech Republic', + x: 167.77, + y: 42.17 + }, + { + name: 'South Korea', + x: 167.52, + y: 50.28, + label: { + anchor: 'left' + } + }, + { + name: 'Croatia', + x: 167.51, + y: 30.69 + }, + { + name: 'Belgium', + x: 162.57, + y: 50.46, + label: { + anchor: 'top', + offsetX: 0, + offsetY: 3 + } + }, + { + name: 'Israel', + x: 160.72, + y: 61.91 + }, + { + name: 'Italy', + x: 160.21, + y: 52.96 + }, + { + name: 'Saudi Arabia', + x: 156.98, + y: 72.12 + }, + { + name: 'Greece', + x: 156.8, + y: 49.1 + }, + { + name: 'Slovakia', + x: 154.13, + y: 44.28 + }, + { + name: 'Taiwan', + x: 150.62, + y: 64.3 + }, + { + name: 'Poland', + x: 150.13, + y: 50.79 + } + ] + } + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-area'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-line-auto-range.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-auto-range.mjs new file mode 100644 index 0000000000..79e0684884 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-auto-range.mjs @@ -0,0 +1,64 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0360 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:横纵坐标的四条标注线自动扩轴。 + * 保留条件:x 字符串 2/5、y 数值 20/220、四个 autoRange 开关。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + x: '2', + autoRange: true + }, + { + x: '5', + autoRange: true + }, + { + y: 20, + autoRange: true + }, + { + y: 220, + autoRange: true + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-line-coordinates.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-coordinates.mjs new file mode 100644 index 0000000000..8d4eb35aa2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-coordinates.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7f0e52a1e9eec95f9ede + * 验证目的:坐标点定位标注线。 + * 保留条件:xField, yField, markLine;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + coordinates: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + } + ], + label: { + text: 'Highlighted data', + autoRotate: true, + position: 'insideMiddleTop', + labelBackground: { + padding: 2, + style: { + fill: '#E8346D' + } + } + }, + endSymbol: { + style: { + visible: false + } + }, + line: { + style: { + stroke: '#E8346D', + lineDash: [], + lineWidth: 2 + } + } + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-line-sum.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-sum.mjs new file mode 100644 index 0000000000..ae83be4302 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-sum.mjs @@ -0,0 +1,60 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 64b6cffbb3874866684fe349 + * 迁移方式:原配置迁移。 + * 验证目的:求和标注线扩展数据范围。 + * 保留条件:数值 x=1..4、y=80/40/10/20、markLine 数组、y=sum、autoRange=true。 + * 覆盖边界:不替换数值坐标为文本类目,不新增标注标签;不宣称复现未提供的历史缺陷。 + */ +export default { + createSpec() { + // 返回原始配置结构的新副本,只移除宿主生命周期依赖。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + y: 'sum', + autoRange: true + } + ] + }; + }, + async verify(page) { + // 精确核对所有原始数据、关联和开关,并检查真实系列状态。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + if (s.getViewData().latestData.reduce((v, r) => v + r.y, 0) !== 150) throw new Error('求和触发数据改变'); + const a = window.__visualChart + .getChart() + .getAllComponents() + .find(c => c.getSpec?.().orient === 'left'); + if (!a || Math.max(...a.getScale().domain()) < 150) throw new Error('求和标注未扩轴'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-line-value.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-value.mjs new file mode 100644 index 0000000000..f1662640a2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-line-value.mjs @@ -0,0 +1,77 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 67fe1cc42b0c7600a7633c8e + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:折线点标签与水平参考线共存。 + * 保留条件:九条原始时间数据、y=9、x=null、红色标注线、标签。 + * 改写说明:仅内联静态 const 引用;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'line', + data: { + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value', + label: { + visible: true + }, + markLine: { + x: null, + line: { + style: { + stroke: '#FF0000' + } + }, + y: 9 + } + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/mark-point-symbol.mjs b/packages/vchart/__tests__/visual/cases/components/marker/mark-point-symbol.mjs new file mode 100644 index 0000000000..285afa07ca --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/mark-point-symbol.mjs @@ -0,0 +1,153 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 683ea46983f5b200a644bb87 + * 验证目的:标注点文字、端点配置与动态轴标签。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:不单独宣称隐藏连接线下的端点符号已显示;仅验证静态最终状态,动画关闭,不代表下钻或悬停已验证。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'bar', + height: 300, + data: [ + { + id: 'barData', + values: [ + { time: '10:20', cost: 2 }, + { time: '10:30', cost: 1 }, + { time: '10:40', cost: 1 }, + { time: '10:50', cost: 2 }, + { time: '11:00', cost: 2 }, + { time: '11:10', cost: 2 }, + { time: '11:20', cost: 1 }, + { time: '11:30', cost: 1 }, + { time: '11:40', cost: 2 }, + { time: '11:50', cost: 1 } + ] + } + ], + xField: 'time', + yField: 'cost', + crosshair: { + xField: { + visible: true, + line: { + type: 'rect', + style: { + fill: 'rgb(85,208,93)', + fillOpacity: 0.1 + } + }, + bindingAxesIndex: [1], + defaultSelect: { + axisIndex: 1, + datum: '10:20' + } + } + }, + label: { + visible: true, + animation: false, + formatMethod: datum => `${datum} minutes`, + style: { + fill: 'rgb(155,155,155)' + } + }, + bar: { + style: { + fill: 'rgb(85,208,93)', + cornerRadius: [4, 4, 0, 0], + width: 30 + } + }, + markPoint: [ + { + coordinate: { + time: '10:20', + cost: 2 + }, + itemContent: { + type: 'text', + autoRotate: false, + offsetY: -10, + text: { + dy: 14, + text: '2 minutes', + style: { + fill: 'white', + fontSize: 14 + }, + labelBackground: { + padding: [5, 10, 5, 10], + style: { + fill: '#000', + cornerRadius: 5 + } + } + } + }, + itemLine: { + endSymbol: { + visible: true, + style: { + angle: Math.PI, + scaleY: 0.4, + fill: '#000', + dy: 4, + stroke: '#000' + } + }, + startSymbol: { visible: false }, + line: { + style: { + visible: false + } + } + } + } + ], + animationUpdate: false, + axes: [ + { + orient: 'left', + max: 10, + label: { visible: false }, + grid: { + style: { lineDash: [4, 4] } + } + }, + { + orient: 'bottom', + label: { + formatMethod: datum => { + return datum === '10:20' ? 'Current' : datum; + }, + style: (datum, a, b) => { + return { + fontSize: datum === '10:20' ? 14 : 12, + fill: datum === '10:20' ? 'black' : 'grey' + }; + } + }, + paddingOuter: 0.5, + paddingInner: 0, + grid: { + visible: true, + alignWithLabel: false, + style: { lineDash: [4, 4] } + } + } + ] + }; + + return spec; + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'mark-point'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-absolute-position.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-absolute-position.mjs new file mode 100644 index 0000000000..c86ce3b9a3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-absolute-position.mjs @@ -0,0 +1,127 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 6524f16aa876fe243b5148c4 + * 验证目的:绝对像素坐标标注线。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + Age: 'Under 5 Years', + Population: 25635 + }, + { + State: 'WY', + Age: '5 to 13 Years', + Population: 1890 + }, + { + State: 'WY', + Age: '14 to 17 Years', + Population: 9314 + }, + { + State: 'DC', + Age: 'Under 5 Years', + Population: 30352 + }, + { + State: 'DC', + Age: '5 to 13 Years', + Population: 20439 + }, + { + State: 'DC', + Age: '14 to 17 Years', + Population: 10225 + }, + { + State: 'VT', + Age: 'Under 5 Years', + Population: 38253 + }, + { + State: 'VT', + Age: '5 to 13 Years', + Population: 42538 + }, + { + State: 'VT', + Age: '14 to 17 Years', + Population: 15757 + }, + { + State: 'ND', + Age: 'Under 5 Years', + Population: 51896 + }, + { + State: 'ND', + Age: '5 to 13 Years', + Population: 67358 + }, + { + State: 'ND', + Age: '14 to 17 Years', + Population: 18794 + }, + { + State: 'AK', + Age: 'Under 5 Years', + Population: 72083 + }, + { + State: 'AK', + Age: '5 to 13 Years', + Population: 85640 + }, + { + State: 'AK', + Age: '14 to 17 Years', + Population: 22153 + } + ] + } + ], + xField: 'State', + yField: 'Population', + seriesField: 'Age', + stack: true, + bar: { + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + }, + markLine: { + positions: [ + { + x: 0, + y: -1 + }, + { + x: 60, + y: 1 + } + ] + } + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-background-shape.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-background-shape.mjs new file mode 100644 index 0000000000..85337b2a60 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-background-shape.mjs @@ -0,0 +1,146 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 669cc20bcede4100c6d5ded6 + * 验证目的:标注线标签自定义背景路径。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'scatter', + padding: [12, 20, 12, 12], + xField: 'x', + yField: 'y', + sizeField: 'z', + size: { + type: 'linear', + range: [20, 80] + }, + axes: [ + { orient: 'bottom', type: 'linear', min: 60, max: 95 }, + { orient: 'left', type: 'linear', min: 0, max: 200 } + ], + point: { + style: { + fillOpacity: 0.25, + lineWidth: 1, + stroke: '#6690F2', + fill: '#6690F2' + } + }, + label: { + visible: true, + position: 'center', + overlap: { + avoidBaseMark: false + }, + style: { + stroke: '#fff', + lineWidth: 1 + } + }, + markLine: [ + { + x: 65, + label: { + visible: true, + position: 'end', + text: 'Safe fat intake 65g/day', + style: { + textAlign: 'left', + textBaseline: 'top', + fill: '#000', + dx: 10 + }, + labelBackground: { + visible: false + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + } + }, + { + y: 50, + label: { + visible: true, + position: 'insideEnd', + text: 'Safe sugar intake 50g/day', + style: { + fill: '#000', + dy: 5 + }, + labelBackground: { + visible: true, + customShape: (data, attrs, path) => { + const { width: textWidth, height } = attrs; + const width = textWidth + 10; + const arrowWidth = 10; + path.beginPath(); + path.moveTo(0, 0); + path.lineTo(width - arrowWidth, 0); + path.lineTo(width, 0.5 * height); + path.lineTo(width - arrowWidth, height); + path.lineTo(0, height); + path.lineTo(0, 0); + path.closePath(); + return path; + }, + style: { + dy: 5, + fill: '#fec76f' + } + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + } + } + ], + tooltip: { + mark: { + title: { + value: datum => datum.country + } + } + }, + data: { + id: 'data', + values: [ + { x: 95, y: 95, z: 13.8, name: 'BE', country: 'Belgium' }, + { x: 86.5, y: 102.9, z: 14.7, name: 'DE', country: 'Germany' }, + { x: 80.8, y: 91.5, z: 15.8, name: 'FI', country: 'Finland' }, + { x: 80.4, y: 102.5, z: 12, name: 'NL', country: 'Netherlands' }, + { x: 80.3, y: 86.1, z: 11.8, name: 'SE', country: 'Sweden' }, + { x: 78.4, y: 70.1, z: 16.6, name: 'ES', country: 'Spain' }, + { x: 74.2, y: 68.5, z: 14.5, name: 'FR', country: 'France' }, + { x: 73.5, y: 83.1, z: 10, name: 'NO', country: 'Norway' }, + { x: 71, y: 93.2, z: 24.7, name: 'UK', country: 'United Kingdom' }, + { x: 69.2, y: 57.6, z: 10.4, name: 'IT', country: 'Italy' }, + { x: 68.6, y: 20, z: 16, name: 'RU', country: 'Russia' }, + { x: 65.5, y: 126.4, z: 35.3, name: 'US', country: 'United States' }, + { x: 65.4, y: 50.8, z: 28.5, name: 'HU', country: 'Hungary' }, + { x: 63.4, y: 51.8, z: 15.4, name: 'PT', country: 'Portugal' }, + { x: 64, y: 82.9, z: 31.3, name: 'NZ', country: 'New Zealand' } + ] + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-coordinate-callbacks.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-coordinate-callbacks.mjs new file mode 100644 index 0000000000..61eb6bc602 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-coordinate-callbacks.mjs @@ -0,0 +1,75 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 656ed91cd91546008d295158 + * 验证目的:依据轴域和数据计算线、区、点坐标。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + data: { + id: 'data2', + values: [ + { x: 1, y: 80 }, + { x: 2, y: 40 }, + { x: 3, y: 10 }, + { x: 4, y: 20 } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + coordinates: [ + { + x: (data, startData, endData, series) => { + const scale = series.getXAxisHelper().getScale(0); + console.log(scale.domain()); + return scale.domain()[1]; + }, + y: (data, startData, endData, series) => { + const scale = series.getYAxisHelper().getScale(); + console.log(scale.domain()); + return scale.domain()[1]; + } + }, + { x: '4', y: 10 } + ] + } + ], + markArea: [ + { + coordinates: [ + { x: data => data[0].x, y: data => data[0].y }, + { x: data => data[1].x, y: data => data[1].y }, + { x: data => data[2].x, y: data => data[2].y }, + { x: data => data[3].x, y: data => data[3].y } + ] + } + ], + markPoint: [ + { + coordinate: { + x: data => data[1].x, + y: data => { + let a = 0; + data.forEach(datum => (a += datum.y)); + return a / data.length; + } + } + }, + { x: '3', y: 50 } + ] + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-end-symbols.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-end-symbols.mjs new file mode 100644 index 0000000000..7f3ece0760 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-end-symbols.mjs @@ -0,0 +1,65 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e034e + * 验证目的:标注线端点符号与显隐组合。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + x: '1', + endSymbol: { + visible: true, + symbolType: 'square', + style: { + fill: 'purple', + stroke: '#fff', + lineWidth: 3, + size: 20 + } + } + }, + { + y: 30, + endSymbol: { + visible: false + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-expand-callback.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-expand-callback.mjs new file mode 100644 index 0000000000..f6f7161316 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-expand-callback.mjs @@ -0,0 +1,118 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 69e8b297dce443005d71325c + * 验证目的:多段标注线扩展距离回调。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'bar', + padding: [12, 80, 12, 12], + data: [ + { + id: 'barData', + values: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '1940', value: 133 }, + { type: 'Autocracies', year: '1950', value: 130 }, + { type: 'Autocracies', year: '1960', value: 126 }, + { type: 'Autocracies', year: '1970', value: 117 }, + { type: 'Autocracies', year: '1980', value: 114 }, + { type: 'Autocracies', year: '1990', value: 111 }, + { type: 'Autocracies', year: '2000', value: 89 }, + { type: 'Autocracies', year: '2010', value: 80 }, + { type: 'Autocracies', year: '2018', value: 80 }, + { type: 'Democracies', year: '1930', value: 22 }, + { type: 'Democracies', year: '1940', value: 13 }, + { type: 'Democracies', year: '1950', value: 25 }, + { type: 'Democracies', year: '1960', value: 29 }, + { type: 'Democracies', year: '1970', value: 38 }, + { type: 'Democracies', year: '1980', value: 41 }, + { type: 'Democracies', year: '1990', value: 57 }, + { type: 'Democracies', year: '2000', value: 87 }, + { type: 'Democracies', year: '2010', value: 98 }, + { type: 'Democracies', year: '2018', value: 99 } + ] + } + ], + xField: 'year', + yField: 'value', + seriesField: 'type', + stack: true, + legends: { + visible: true, + orient: 'top', + position: 'start' + }, + markLine: { + type: 'type-step', + coordinates: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '2000', value: 80 } + ], + connectDirection: 'right', + expandDistance: (markerData, ctx) => { + const { region, startRegion, endRegion, coordinatePoints } = ctx; + const [p0, p1] = coordinatePoints; + console.log(3333, 'markerData', markerData, region); + const regionEndPos = region.getLayoutStartPoint().x + region.getLayoutRect().width; + const gapY = regionEndPos - p1.x; + return gapY; + }, + line: { + multiSegment: true, + mainSegmentIndex: 1, + style: [ + { + lineDash: [2, 2], + stroke: '#000', + lineWidth: 2 + }, + { + stroke: '#000', + lineWidth: 2 + }, + { + lineDash: [2, 2], + stroke: '#000', + lineWidth: 2 + } + ] + }, + label: { + position: 'middle', + text: `${(((80 - 129) / 80) * 100).toFixed(0)}%`, + labelBackground: { + padding: { left: 4, right: 4, top: 4, bottom: 4 }, + style: { + fill: '#fff', + fillOpacity: 1, + stroke: '#000', + lineWidth: 1, + cornerRadius: 4 + } + }, + style: { + fill: '#000' + } + }, + endSymbol: { + size: 12, + refX: -4 + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-extension-quadrants.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-extension-quadrants.mjs new file mode 100644 index 0000000000..d41bf6b5cd --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-extension-quadrants.mjs @@ -0,0 +1,234 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f0d52a1e9eec95f9edb + * 验证目的:散点象限标区与依赖 region 布局的扩展图元。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'scatter', + xField: 'x', + yField: 'y', + padding: [12, 12, 50, 50], + region: [{ style: { stroke: '#000', lineWidth: 2 } }], + label: { visible: true, style: { fill: '#222' } }, + axes: [ + { orient: 'bottom', type: 'linear', range: { min: 140, max: 220 }, visible: false }, + { orient: 'left', max: 85, visible: false } + ], + markArea: [ + { + x: '50%', + x1: '100%', + y: '0%', + y1: '50%', + area: { style: { fill: '#5B8FF9', fillOpacity: 0.15 } }, + label: { + text: 'Quadrant 1', + position: 'insideTop', + dy: 4, + labelBackground: { style: { fill: '#FFC400' } }, + style: { fill: '#fff' } + } + }, + { + x: '0%', + x1: '50%', + y: '0%', + y1: '50%', + area: { style: { fill: '#fff' } }, + label: { + text: 'Quadrant 2', + position: 'insideTop', + dy: 4, + labelBackground: { style: { fill: '#FFC400' } }, + style: { fill: '#fff' } + } + }, + { + x: '0%', + x1: '50%', + y: '50%', + y1: '100%', + area: { style: { fill: '#5B8FF9', fillOpacity: 0.15 } }, + label: { + text: 'Quadrant 3', + position: 'insideBottom', + dy: -4, + labelBackground: { style: { fill: '#FFC400' } }, + style: { fill: '#fff' } + } + }, + { + x: '50%', + x1: '100%', + y: '50%', + y1: '100%', + area: { style: { fill: '#fff' } }, + label: { + text: 'Quadrant 4', + position: 'insideBottom', + dy: -4, + labelBackground: { style: { fill: '#FFC400' } }, + style: { fill: '#fff' } + } + } + ], + markLine: [ + { x: '50%', line: { style: { lineDash: [0], stroke: '#ccc' } }, endSymbol: { visible: false } }, + { y: '50%', line: { style: { lineDash: [0], stroke: '#ccc' } }, endSymbol: { visible: false } }, + { + x: '0%', + y: '100%', + x1: '100%', + y1: '0%', + line: { style: { lineDash: [0], stroke: '#ccc' } }, + endSymbol: { visible: false } + }, + { + x: '0%', + y: '0%', + x1: '100%', + y1: '100%', + line: { style: { lineDash: [0], stroke: '#ccc' } }, + endSymbol: { visible: false } + } + ], + extensionMark: [ + { + type: 'group', + visible: true, + style: { + x: -12, + y: (datum, ctx, elements, dataView) => { + return ctx.getRegion().getLayoutRect().height; + } + }, + children: [ + { + type: 'text', + visible: true, + style: { + x: 0, + y: 0, + fontSize: 14, + lineHeight: 16, + fill: '#000', + text: 'ABILITY TO EXECUTE', + textAlign: 'start', + textBaseline: 'middle', + angle: -90 + } + }, + { + type: 'rule', + visible: true, + style: { + x: 0, + y: (datum, ctx, elements, dataView) => { + return -150; + }, + x1: 0, + y1: (datum, ctx, elements, dataView) => { + return -190; + }, + stroke: '#000', + lineWidth: 1 + } + }, + { + type: 'symbol', + visible: true, + style: { + x: 0, + y: (datum, ctx, elements, dataView) => { + return -186; + }, + symbolType: 'triangleUp', + fill: '#000', + size: 12 + } + } + ] + }, + { + type: 'group', + visible: true, + style: { + x: 0, + y: (datum, ctx, elements, dataView) => { + return ctx.getRegion().getLayoutRect().height + 12; + } + }, + children: [ + { + type: 'text', + visible: true, + style: { + x: 0, + y: 0, + fontSize: 14, + lineHeight: 16, + fill: '#000', + text: 'COMPLETENESS OF VISION', + textAlign: 'start', + textBaseline: 'middle' + } + }, + { type: 'rule', visible: true, style: { x: 190, y: 0, x1: 230, y1: 0, stroke: '#000', lineWidth: 1 } }, + { + type: 'symbol', + visible: true, + style: { x: 236, y: 0, symbolType: 'triangleRight', fill: '#000', size: 12 } + } + ] + } + ], + data: { + id: 'data2', + values: [ + { name: 'Denmark', x: 201.53, y: 26.84 }, + { name: 'Switzerland', x: 196.44, y: 21.73 }, + { name: 'Australia', x: 196.4, y: 24.09 }, + { name: 'New Zealand', x: 196.09, y: 19.43, label: { anchor: 'left' } }, + { name: 'Germany', x: 189.87, y: 27.68 }, + { name: 'Austria', x: 187, y: 25.43 }, + { name: 'Netherlands', x: 186.46, y: 29.08, label: { anchor: 'left' } }, + { name: 'Spain', x: 184.69, y: 40.37, label: { anchor: 'left' } }, + { name: 'Finland', x: 183.98, y: 14.57 }, + { name: 'United States', x: 181.91, y: 32.73, label: { anchor: 'left' } }, + { name: 'Portugal', x: 180.66, y: 29.87 }, + { name: 'Sweden', x: 177.93, y: 16.59 }, + { name: 'United Kingdom', x: 177.73, y: 34.24 }, + { name: 'Norway', x: 176.23, y: 19.28, label: { anchor: 'left' } }, + { name: 'Canada', x: 172.83, y: 28.17, label: { anchor: 'left' } }, + { name: 'Japan', x: 172.72, y: 40.9 }, + { name: 'France', x: 172.3, y: 42.04, label: { anchor: 'left' } }, + { name: 'Estonia', x: 171.71, y: 19.19 }, + { name: 'Ireland', x: 170.83, y: 27.47 }, + { name: 'Czech Republic', x: 167.77, y: 42.17 }, + { name: 'South Korea', x: 167.52, y: 50.28, label: { anchor: 'left' } }, + { name: 'Croatia', x: 167.51, y: 30.69 }, + { name: 'Belgium', x: 162.57, y: 50.46, label: { anchor: 'top', offsetX: 0, offsetY: 3 } }, + { name: 'Israel', x: 160.72, y: 61.91 }, + { name: 'Italy', x: 160.21, y: 52.96 }, + { name: 'Saudi Arabia', x: 156.98, y: 72.12 }, + { name: 'Greece', x: 156.8, y: 49.1 }, + { name: 'Slovakia', x: 154.13, y: 44.28 }, + { name: 'Taiwan', x: 150.62, y: 64.3 }, + { name: 'Poland', x: 150.13, y: 50.79 } + ] + } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-format-statistics.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-format-statistics.mjs new file mode 100644 index 0000000000..fd5b92896c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-format-statistics.mjs @@ -0,0 +1,156 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 64ccc170d28093658a4ab0ff + * 验证目的:统计标注区域与标注线标签回调。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'line', + xField: 'x', + yField: 'y', + seriesField: 'type', + data: { + id: 'data2', + values: [ + { x: 'Mon', y: 14000, type: 'A' }, + { x: 'Tue', y: 14500, type: 'A' }, + { x: 'Wed', y: 24000, type: 'A' }, + { x: 'Thu', y: 13000, type: 'A' }, + { x: 'Fri', y: 15000, type: 'A' }, + { x: 'Sat', y: 19000, type: 'A' }, + { x: 'Sun', y: 21000, type: 'A' }, + { x: 'Mon', y: 15000, type: 'B' }, + { x: 'Tue', y: 14800, type: 'B' }, + { x: 'Wed', y: 25000, type: 'B' }, + { x: 'Thu', y: 9000, type: 'B' }, + { x: 'Fri', y: 15000, type: 'B' }, + { x: 'Sat', y: 20000, type: 'B' }, + { x: 'Sun', y: 19000, type: 'B' } + ] + }, + markLine: [ + { + x: (relativeSeriesData, startRelativeSeriesData, endRelativeSeriesData) => { + console.log('relativeSeriesData', relativeSeriesData); + console.log('startRelativeSeriesData', startRelativeSeriesData); + console.log('endRelativeSeriesData', endRelativeSeriesData); + return 'Wed'; + }, + label: { + text: 'National holiday', + position: 'insideEndBottom', + reY: 10, + labelBackground: { + padding: 5, + style: { + stroke: '#6690F2', + fillOpacity: 0 + } + }, + style: { + fill: '#6690F2' + } + }, + line: { + style: { + stroke: '#6690F2', + lineDash: [] + } + }, + endSymbol: { + style: {} + } + }, + { + y: 'average', + label: { + position: 'insideEndBottom', + refY: -10, + formatMethod: datum => { + console.log('caculate-datum', datum); + return 'Average Visit Num:' + datum?.[0]['y']; + }, + labelBackground: { + padding: 2, + style: { + fill: '#6690F2' + } + }, + style: { + fontSize: 12 + } + }, + line: { + style: { + stroke: '#6690F2', + lineDash: [] + } + }, + startSymbol: { + visible: true, + symbolType: 'triangleDown', + autoRotate: false, + style: { + visible: true + } + }, + endSymbol: { + style: {} + } + } + ], + markArea: [ + { + y: 'average', + y1: 'min', + label: { + position: 'insideEndBottom', + refY: -10, + formatMethod: datum => { + console.log('datum', datum?.[0]['y']); + return 'Average Visit Num:' + datum?.[0]['y']; + }, + labelBackground: { + padding: 2, + style: { + fill: '#6690F2' + } + }, + style: { + fontSize: 12 + } + }, + line: { + style: { + stroke: '#6690F2', + lineDash: [] + } + }, + endSymbol: { + style: { + visible: false + } + } + } + ], + line: { + style: { + curveType: 'monotone' + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-label-background.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-label-background.mjs new file mode 100644 index 0000000000..0e7873d249 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-label-background.mjs @@ -0,0 +1,95 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e035d + * 验证目的:标注线标签背景与偏移布局。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + x: '1', + label: { + visible: true, + text: 'test middle333', + refX: -20, + refY: -10, + style: { + fontSize: 24, + fontWeight: 'bold', + fontStyle: 'italic', + fontFamily: 'pingfang-sc', + fill: '#333', + stroke: '#fff', + lineWidth: 6 + }, + labelBackground: { + visible: false + } + } + }, + { + y: 30, + label: { + visible: true, + text: 'test middle333', + position: 'insideEndBottom', + refX: 0, + refY: -20, + style: { + fontSize: 24, + fontWeight: 'bold', + fontStyle: 'italic', + fontFamily: 'pingfang-sc', + fill: '#333', + stroke: '#fff', + lineWidth: 6 + }, + labelBackground: { + visible: true, + padding: [6, 6, 8, 8], + style: { + fill: 'red', + borderRadius: [10, 10, 10, 10], + cornerRadius: [10, 10, 10, 10] + } + } + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-label-confine.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-label-confine.mjs new file mode 100644 index 0000000000..cefb855e01 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-label-confine.mjs @@ -0,0 +1,127 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 6501760241adc800895c20f4 + * 验证目的:标注线标签边界约束与端点符号。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'line', + data: { + id: 'line', + values: [ + { + time: '2:00', + value: 8 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value', + markLine: [ + { + y: data => { + console.log('y', data); + return 20; + }, + startSymbol: { + visible: true, + symbolType: 'triangleDown', + style: { + size: 10, + fill: '#f3a016' + } + }, + endSymbol: { + visible: false + }, + autoRange: true, + label: { + visible: true, + text: 'This is a label', + confine: true, + style: { + dx: -4, + dy: 0, + fontSize: 12, + fontWeight: 'normal', + fill: '#fff', + cursor: 'pointer' + }, + position: 'insideStartTop', + labelBackground: { + visible: true, + padding: { + left: 5, + right: 5, + top: 2, + bottom: 2 + }, + style: { + fill: '#2F3B52', + fillOpacity: 0.9, + dx: -4, + dy: 0 + } + } + }, + line: { + style: { + stroke: '#f3a016', + lineWidth: 2, + lineDash: [3, 3], + cursor: 'pointer' + } + }, + relativeSeriesId: 'mainSeries', + id: '7d14708c-de9d-49a1-919a-26c65ff95b42', + interactive: true + } + ] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-monotone-line.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-monotone-line.mjs new file mode 100644 index 0000000000..b4ceb92a48 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-monotone-line.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65fa95f0855a8400b5ecdd04 + * 验证目的:标注线单调曲线路径。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + coordinates: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 20 + } + ], + label: { + text: 'Some data is highlighted', + autoRotate: true, + position: 'insideMiddleTop', + labelBackground: { + padding: 2, + style: { + fill: '#E8346D' + } + } + }, + endSymbol: { + style: { + visible: false + } + }, + line: { + style: { + stroke: '#E8346D', + lineDash: [], + lineWidth: 2, + curveType: 'monotoneX' + } + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-multi-segment.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-multi-segment.mjs new file mode 100644 index 0000000000..8dcc004e65 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-multi-segment.mjs @@ -0,0 +1,112 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 655c9331ce24cd5a7fc9796b + * 验证目的:多段标注线连接方向及扩展距离。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'bar', + padding: [12, 80, 12, 12], + data: [ + { + id: 'barData', + values: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '1940', value: 133 }, + { type: 'Autocracies', year: '1950', value: 130 }, + { type: 'Autocracies', year: '1960', value: 126 }, + { type: 'Autocracies', year: '1970', value: 117 }, + { type: 'Autocracies', year: '1980', value: 114 }, + { type: 'Autocracies', year: '1990', value: 111 }, + { type: 'Autocracies', year: '2000', value: 89 }, + { type: 'Autocracies', year: '2010', value: 80 }, + { type: 'Autocracies', year: '2018', value: 80 }, + { type: 'Democracies', year: '1930', value: 22 }, + { type: 'Democracies', year: '1940', value: 13 }, + { type: 'Democracies', year: '1950', value: 25 }, + { type: 'Democracies', year: '1960', value: 29 }, + { type: 'Democracies', year: '1970', value: 38 }, + { type: 'Democracies', year: '1980', value: 41 }, + { type: 'Democracies', year: '1990', value: 57 }, + { type: 'Democracies', year: '2000', value: 87 }, + { type: 'Democracies', year: '2010', value: 98 }, + { type: 'Democracies', year: '2018', value: 99 } + ] + } + ], + xField: 'year', + yField: 'value', + seriesField: 'type', + stack: true, + legends: { + visible: true, + orient: 'top', + position: 'start' + }, + markLine: { + type: 'type-step', + coordinates: [ + { type: 'Autocracies', year: '1930', value: 129 }, + { type: 'Autocracies', year: '2018', value: 80 } + ], + connectDirection: 'right', + expandDistance: 80, + line: { + multiSegment: true, + mainSegmentIndex: 1, + style: [ + { + lineDash: [2, 2], + stroke: '#000', + lineWidth: 2 + }, + { + stroke: '#000', + lineWidth: 2 + }, + { + lineDash: [2, 2], + stroke: '#000', + lineWidth: 2 + } + ] + }, + label: { + position: 'middle', + text: `${(((80 - 129) / 80) * 100).toFixed(0)}%`, + labelBackground: { + padding: { left: 4, right: 4, top: 4, bottom: 4 }, + style: { + fill: '#fff', + fillOpacity: 1, + stroke: '#000', + lineWidth: 1, + cornerRadius: 4 + } + }, + style: { + fill: '#000' + }, + refY: 0 + }, + endSymbol: { + size: 12, + refX: -4 + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-polar-coordinates.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-polar-coordinates.mjs new file mode 100644 index 0000000000..bc69a8646f --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-polar-coordinates.mjs @@ -0,0 +1,391 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66385433b19f5100ced6a64f + * 验证目的:极坐标角度半径标注线和标注点。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + data: [ + { + id: 'id0', + values: [ + { + type: 'MIDNIGHT', + value: 1 + }, + { + type: '1 AM', + value: 1 + }, + { + type: '2 AM', + value: 1 + }, + { + type: '3 AM', + value: 1 + }, + { + type: '4 AM', + value: 1 + }, + { + type: '5 AM', + value: 1 + }, + { + type: '6 AM', + value: 1 + }, + { + type: '7 AM', + value: 1 + }, + { + type: '8 AM', + value: 1 + }, + { + type: '9 AM', + value: 1 + }, + { + type: '10 AM', + value: 1 + }, + { + type: '11 AM', + value: 1 + }, + { + type: 'NOON', + value: 1 + }, + { + type: '13 AM', + value: 1 + }, + { + type: '14 AM', + value: 1 + }, + { + type: '15 AM', + value: 1 + }, + { + type: '16 AM', + value: 1 + }, + { + type: '17 AM', + value: 1 + }, + { + type: '18 AM', + value: 1 + }, + { + type: '19 AM', + value: 1 + }, + { + type: '20 AM', + value: 1 + }, + { + type: '21 AM', + value: 1 + }, + { + type: '22 AM', + value: 1 + }, + { + type: '23 AM', + value: 1 + } + ] + }, + { + id: 'id1', + values: [ + { + type: 'a', + value: 1 + } + ] + } + ], + dataIndex: 0, + type: 'rose', + outerRadius: 0.7, + innerRadius: 0.5, + valueField: 'value', + categoryField: 'type', + seriesField: 'type', + startAngle: -97.5, + title: { + text: 'History of Earth in 24-hour clock', + textStyle: { + height: 50, + lineWidth: 2, + fill: '#333', + fontSize: 20, + fontFamily: 'Times New Roman' + }, + subtextStyle: { + character: [ + { + text: '', + fontFamily: 'Times New Roman', + fontSize: 14, + fill: '#333' + } + ] + } + }, + axes: [ + { + orient: 'angle', + type: 'band', + zIndex: 999, + tick: { + visible: true, + tickSize: 10, + inside: true, + style: { + stroke: '#fff' + } + }, + label: { + visible: true, + inside: true, + style: { + fill: '#fff' + } + }, + grid: { + visible: true, + style: { + lineDash: [0], + stroke: '#fff', + lineWidth: 1 + }, + alignWithLabel: false + }, + style: { + zIndex: 400 + } + } + ], + label: { + visible: false, + position: 'inside' + }, + tooltip: { + visible: false + }, + rose: { + style: { + fill: 'rgb(129, 216, 208)' + } + }, + markLine: [ + { + radius: 1.1, + angle: 'MIDNIGHT', + angle1: '3 AM' + }, + { + radius: 1.1, + angle: '6 AM', + angle1: '13 AM' + } + ], + markPoint: [ + { + position: { + x: '50%', + y: '50%' + }, + regionRelative: true, + itemLine: { + visible: false + }, + itemContent: { + type: 'symbol', + symbol: { + style: { + dx: -80, + dy: -40, + size: 60, + symbolType: + '', + fill: 'rgb(129, 216, 208)' + } + } + } + }, + { + angle: '4 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: 5, + offsetY: -20, + text: { + text: '4: 00 Origin of life' + } + } + }, + { + angle: '5 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: 5, + offsetY: -20, + text: { + text: '5: 00 Oldest Fossils' + } + } + }, + { + angle: '14 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: 20, + text: { + text: 'Single-Celled Algae(Acritarchs)', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '18 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: -20, + text: { + text: 'Sexual Reproduction', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '20 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: -20, + text: { + text: 'Seaweeds', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '21 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: -20, + text: { + text: 'Trilobites', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '22 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: -20, + text: { + text: 'Coal Swamps', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '23 AM', + radius: 1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: -5, + offsetY: -20, + text: { + text: 'Humans', + style: { + textAlign: 'right' + } + } + } + }, + { + angle: '2 AM', + radius: 1.1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: 10, + offsetY: -20, + text: { + text: 'Meteorite Bombardment 0:00 to 3 am' + } + } + }, + { + angle: '10 AM', + radius: 1.1, + itemContent: { + type: 'text', + autoRotate: false, + offsetX: 10, + offsetY: 20, + text: { + text: '6:00 to 1: Abundant Banded Iron- Formations' + } + } + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-region-position.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-region-position.mjs new file mode 100644 index 0000000000..0b589b0f26 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-region-position.mjs @@ -0,0 +1,59 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 656ed882f69e0e008d9809e3 + * 验证目的:区域相对百分比坐标标注线。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + positions: [ + { + x: '0%', + y: '50%' + }, + { + x: '100%', + y: '50%' + } + ], + regionRelative: true + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/marker/marker-statistics.mjs b/packages/vchart/__tests__/visual/cases/components/marker/marker-statistics.mjs new file mode 100644 index 0000000000..266bbe7bb9 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/marker/marker-statistics.mjs @@ -0,0 +1,62 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0361 + * 验证目的:最小最大和平均值标注线。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'line', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + markLine: [ + { + y: 'sum', + autoRange: true + }, + { + y: 'average', + autoRange: true + }, + { + y: 'min', + autoRange: true + }, + { + y: 'max', + autoRange: true + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'mark-line'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/marker-area-richtext.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/marker-area-richtext.mjs new file mode 100644 index 0000000000..d19a08d512 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/marker-area-richtext.mjs @@ -0,0 +1,223 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 655ad3949ea0da0095642847 + * 验证目的:标区内部富文本标签的排版。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + xField: 'Date', + yField: 'Price', + point: { visible: false }, + line: { style: { lineWidth: 3, lineCap: 'round' } }, + axes: [ + { + orient: 'left', + min: 0.12, + max: 0.18, + tick: { visible: false }, + grid: { style: { lineDash: [0, 0] } }, + domainLine: { visible: false }, + label: { + formatMethod: (value, datum) => { + if (value === 0.18) { + return `$${value}`; + } + return value; + } + } + }, + { + orient: 'bottom', + label: { + formatMethod: (value, datum) => { + if (value.startsWith('Jan-')) { + return `20${value.split('-')[1]}`; + } + return ''; + } + } + } + ], + markArea: [ + { + coordinates: [ + { + Date: 'Jan-20', + Price: 0.18 + }, + { + Date: 'Mar-23', + Price: 0.18 + }, + { + Date: 'Mar-23', + Price: 0.12 + }, + { + Date: 'Jan-20', + Price: 0.12 + } + ], + label: { + type: 'rich', + text: [ + { + text: 'RICHTEXT', + fontWeight: 'bold', + fontSize: 25, + fill: '#3f51b5' + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ], + position: 'insideTop' + } + } + ], + data: { + id: 'data', + values: [ + { Date: 'Jan-13', Price: 0.129 }, + { Date: 'Feb-13', Price: 0.129 }, + { Date: 'Mar-13', Price: 0.128 }, + { Date: 'Apr-13', Price: 0.128 }, + { Date: 'May-13', Price: 0.131 }, + { Date: 'Jun-13', Price: 0.137 }, + { Date: 'Jul-13', Price: 0.137 }, + { Date: 'Aug-13', Price: 0.137 }, + { Date: 'Sep-13', Price: 0.137 }, + { Date: 'Oct-13', Price: 0.132 }, + { Date: 'Nov-13', Price: 0.13 }, + { Date: 'Dec-13', Price: 0.131 }, + { Date: 'Jan-14', Price: 0.134 }, + { Date: 'Feb-14', Price: 0.134 }, + { Date: 'Mar-14', Price: 0.135 }, + { Date: 'Apr-14', Price: 0.131 }, + { Date: 'May-14', Price: 0.136 }, + { Date: 'Jun-14', Price: 0.143 }, + { Date: 'Jul-14', Price: 0.143 }, + { Date: 'Aug-14', Price: 0.143 }, + { Date: 'Sep-14', Price: 0.141 }, + { Date: 'Oct-14', Price: 0.136 }, + { Date: 'Nov-14', Price: 0.134 }, + { Date: 'Dec-14', Price: 0.135 }, + { Date: 'Jan-15', Price: 0.138 }, + { Date: 'Feb-15', Price: 0.138 }, + { Date: 'Mar-15', Price: 0.136 }, + { Date: 'Apr-15', Price: 0.137 }, + { Date: 'May-15', Price: 0.137 }, + { Date: 'Jun-15', Price: 0.143 }, + { Date: 'Jul-15', Price: 0.142 }, + { Date: 'Aug-15', Price: 0.142 }, + { Date: 'Sep-15', Price: 0.141 }, + { Date: 'Oct-15', Price: 0.136 }, + { Date: 'Nov-15', Price: 0.134 }, + { Date: 'Dec-15', Price: 0.133 }, + { Date: 'Jan-16', Price: 0.134 }, + { Date: 'Feb-16', Price: 0.134 }, + { Date: 'Mar-16', Price: 0.134 }, + { Date: 'Apr-16', Price: 0.134 }, + { Date: 'May-16', Price: 0.133 }, + { Date: 'Jun-16', Price: 0.138 }, + { Date: 'Jul-16', Price: 0.139 }, + { Date: 'Aug-16', Price: 0.139 }, + { Date: 'Sep-16', Price: 0.139 }, + { Date: 'Oct-16', Price: 0.134 }, + { Date: 'Nov-16', Price: 0.131 }, + { Date: 'Dec-16', Price: 0.133 }, + { Date: 'Jan-17', Price: 0.134 }, + { Date: 'Feb-17', Price: 0.135 }, + { Date: 'Mar-17', Price: 0.134 }, + { Date: 'Apr-17', Price: 0.135 }, + { Date: 'May-17', Price: 0.137 }, + { Date: 'Jun-17', Price: 0.142 }, + { Date: 'Jul-17', Price: 0.143 }, + { Date: 'Aug-17', Price: 0.142 }, + { Date: 'Sep-17', Price: 0.142 }, + { Date: 'Oct-17', Price: 0.137 }, + { Date: 'Nov-17', Price: 0.136 }, + { Date: 'Dec-17', Price: 0.136 }, + { Date: 'Jan-18', Price: 0.135 }, + { Date: 'Feb-18', Price: 0.135 }, + { Date: 'Mar-18', Price: 0.135 }, + { Date: 'Apr-18', Price: 0.134 }, + { Date: 'May-18', Price: 0.136 }, + { Date: 'Jun-18', Price: 0.139 }, + { Date: 'Jul-18', Price: 0.139 }, + { Date: 'Aug-18', Price: 0.139 }, + { Date: 'Sep-18', Price: 0.138 }, + { Date: 'Oct-18', Price: 0.136 }, + { Date: 'Nov-18', Price: 0.134 }, + { Date: 'Dec-18', Price: 0.135 }, + { Date: 'Jan-19', Price: 0.135 }, + { Date: 'Feb-19', Price: 0.136 }, + { Date: 'Mar-19', Price: 0.135 }, + { Date: 'Apr-19', Price: 0.135 }, + { Date: 'May-19', Price: 0.136 }, + { Date: 'Jun-19', Price: 0.139 }, + { Date: 'Jul-19', Price: 0.14 }, + { Date: 'Aug-19', Price: 0.139 }, + { Date: 'Sep-19', Price: 0.139 }, + { Date: 'Oct-19', Price: 0.136 }, + { Date: 'Nov-19', Price: 0.133 }, + { Date: 'Dec-19', Price: 0.133 }, + { Date: 'Jan-20', Price: 0.134 }, + { Date: 'Feb-20', Price: 0.134 }, + { Date: 'Mar-20', Price: 0.134 }, + { Date: 'Apr-20', Price: 0.133 }, + { Date: 'May-20', Price: 0.134 }, + { Date: 'Jun-20', Price: 0.137 }, + { Date: 'Jul-20', Price: 0.137 }, + { Date: 'Aug-20', Price: 0.137 }, + { Date: 'Sep-20', Price: 0.137 }, + { Date: 'Oct-20', Price: 0.135 }, + { Date: 'Nov-20', Price: 0.136 }, + { Date: 'Dec-20', Price: 0.136 }, + { Date: 'Jan-21', Price: 0.136 }, + { Date: 'Feb-21', Price: 0.137 }, + { Date: 'Mar-21', Price: 0.138 }, + { Date: 'Apr-21', Price: 0.139 }, + { Date: 'May-21', Price: 0.14 }, + { Date: 'Jun-21', Price: 0.142 }, + { Date: 'Jul-21', Price: 0.143 }, + { Date: 'Aug-21', Price: 0.144 }, + { Date: 'Sep-21', Price: 0.144 }, + { Date: 'Oct-21', Price: 0.142 }, + { Date: 'Nov-21', Price: 0.142 }, + { Date: 'Dec-21', Price: 0.142 }, + { Date: 'Jan-22', Price: 0.147 }, + { Date: 'Feb-22', Price: 0.148 }, + { Date: 'Mar-22', Price: 0.15 }, + { Date: 'Apr-22', Price: 0.151 }, + { Date: 'May-22', Price: 0.154 }, + { Date: 'Jun-22', Price: 0.16 }, + { Date: 'Jul-22', Price: 0.164 }, + { Date: 'Aug-22', Price: 0.167 }, + { Date: 'Sep-22', Price: 0.167 }, + { Date: 'Oct-22', Price: 0.166 }, + { Date: 'Nov-22', Price: 0.163 }, + { Date: 'Dec-22', Price: 0.165 }, + { Date: 'Jan-23', Price: 0.168 }, + { Date: 'Feb-23', Price: 0.168 }, + { Date: 'Mar-23', Price: 0.166 } + ] + } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-content.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-content.mjs new file mode 100644 index 0000000000..cf8150885c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-content.mjs @@ -0,0 +1,204 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 655ad2b09ea0da0095642845 + * 验证目的:富文本标注的行内样式与独立末端内容。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + xField: 'year', + yField: 'population', + axes: [{ orient: 'bottom', type: 'linear', min: 1868, max: 2016 }], + markPoint: [ + { + coordinate: { year: '1878', population: 100 }, + itemContent: { + offsetY: -100, + autoRotate: false, + textStyle: { + type: 'rich', + text: [ + { text: 'RICHTEXT', fontWeight: 'bold', fontSize: 25, fill: '#3f51b5' }, + { text: 'Alternative', fontStyle: 'italic', textDecoration: 'underline', fill: '#3f51b5' } + ] + } + }, + itemLine: { + decorativeLine: { visible: true }, + startSymbol: { size: 20, style: { stroke: '#E8346D' } }, + line: { style: { stroke: '#E8346D' } } + } + }, + { + coordinate: { year: '2016', population: 899447 }, + itemContent: { + offsetY: 40, + offsetX: -200, + type: 'richText', + autoRotate: false, + position: 'insideBottom', + richText: { + style: { + width: 100, + height: 50, + dx: 110, + dy: 10, + textConfig: [ + { text: '2016\n', fontWeight: 'bold', fontSize: 13, fill: '#E8346D', fontFamily: 'Times New Roman' }, + { text: 'population was 899447', fontSize: 10, fill: '#E8346D', fontFamily: 'Times New Roman' } + ] + } + } + }, + itemLine: { + type: 'type-do', + startSymbol: { size: 24, style: { stroke: '#E8346D' } }, + line: { style: { stroke: '#E8346D' } } + } + } + ], + point: { style: { visible: false } }, + line: { style: { lineWidth: 2 } }, + title: { + text: 'Population History - Line Chart', + textStyle: { lineWidth: 3, fill: '#333', fontSize: 20, fontFamily: 'Times New Roman' } + }, + data: { + id: 'data2', + values: [ + { census_year: true, year: 2016, source: 'CoE', population: 899447 }, + { census_year: true, year: 2014, source: 'CoE', population: 877926 }, + { year: 2013, source: 'CoE 6', population: 847712 }, + { census_year: true, year: 2012, source: 'CoE', population: 817498 }, + { year: 2010, source: 'CoE 6', population: 797320 }, + { census_year: true, year: 2009, source: 'CoE', population: 782439 }, + { census_year: true, year: 2008, source: 'CoE', population: 752412 }, + { year: 2007, source: 'CoE 6', population: 741392 }, + { census_year: true, year: 2005, source: 'CoE', population: 712391 }, + { year: 2004, source: 'CoE 6', population: 698631 }, + { year: 2003, source: 'CoE 6', population: 684871 }, + { year: 2002, source: 'CoE 6', population: 671110 }, + { year: 2000, source: 'CoE 6', population: 652817 }, + { census_year: true, year: 1999, source: 'CoE', population: 648284 }, + { year: 1998, source: 'CoE 6', population: 637625 }, + { year: 1997, source: 'CoE 6', population: 626965 }, + { year: 1995, source: 'CoE 6', population: 619870 }, + { year: 1994, source: 'CoE 6', population: 623435 }, + { census_year: true, year: 1993, source: 'CoE', population: 626999 }, + { census_year: true, year: 1992, source: 'CoE', population: 618195 }, + { census_year: true, year: 1990, source: 'CoE', population: 605538 }, + { census_year: true, year: 1989, source: 'CoE', population: 583872 }, + { year: 1988, source: 'CoE', population: 580061 }, + { census_year: true, year: 1987, source: 'CoE', population: 576249 }, + { year: 1985, source: 'CoE 6', population: 567699 }, + { year: 1984, source: 'CoE 6', population: 563892 }, + { census_year: true, year: 1983, source: 'CoE', population: 560085 }, + { census_year: true, year: 1982, source: 'CoE 1', population: 551314 }, + { census_year: true, year: 1980, source: 'CoE', population: 505773 }, + { census_year: true, year: 1979, source: 'CoE', population: 491359 }, + { census_year: true, year: 1978, source: 'CoE', population: 478066 }, + { census_year: true, year: 1977, source: 'CoE', population: 471474 }, + { census_year: true, year: 1975, source: 'CoE', population: 451635 }, + { census_year: true, year: 1974, source: 'CoE', population: 445691 }, + { census_year: true, year: 1973, source: 'CoE', population: 442365 }, + { census_year: true, year: 1972, source: 'CoE', population: 441530 }, + { census_year: true, year: 1970, source: 'CoE', population: 429750 }, + { census_year: true, year: 1969, source: 'CoE', population: 422418 }, + { census_year: true, year: 1968, source: 'CoE', population: 410105 }, + { census_year: true, year: 1967, source: 'CoE', population: 393593 }, + { census_year: true, year: 1965, source: 'CoE', population: 371265 }, + { census_year: true, year: 1964, source: 'CoE 2', population: 311804 }, + { census_year: true, year: 1963, source: 'CoE', population: 303756 }, + { census_year: true, year: 1962, source: 'CoE', population: 294967 }, + { census_year: true, year: 1960, source: 'CoE', population: 269314 }, + { census_year: true, year: 1959, source: 'CoE', population: 260733 }, + { census_year: true, year: 1958, source: 'CoE', population: 252131 }, + { census_year: true, year: 1957, source: 'CoE', population: 238353 }, + { census_year: true, year: 1955, source: 'CoE', population: 209353 }, + { census_year: true, year: 1954, source: 'CoE', population: 197836 }, + { census_year: true, year: 1953, source: 'CoE', population: 183411 }, + { census_year: true, year: 1952, source: 'CoE', population: 169196 }, + { census_year: true, year: 1950, source: 'CoE', population: 148861 }, + { census_year: true, year: 1949, source: 'CoE', population: 137469 }, + { census_year: true, year: 1948, source: 'CoE', population: 126609 }, + { census_year: true, year: 1947, source: 'CoE', population: 118541 }, + { census_year: true, year: 1946, source: 'CoE', population: 114976 }, + { census_year: true, year: 1945, source: 'CoE', population: 111745 }, + { census_year: true, year: 1944, source: 'CoE', population: 108416 }, + { census_year: true, year: 1943, source: 'CoE', population: 105536 }, + { census_year: true, year: 1942, source: 'CoE', population: 96725 }, + { census_year: true, year: 1940, source: 'CoE', population: 91723 }, + { census_year: true, year: 1939, source: 'CoE', population: 90419 }, + { census_year: true, year: 1938, source: 'CoE', population: 88887 }, + { census_year: true, year: 1937, source: 'CoE', population: 87034 }, + { census_year: true, year: 1936, source: 'CoE', population: 85470 }, + { census_year: true, year: 1935, source: 'CoE', population: 82634 }, + { census_year: true, year: 1934, source: 'CoE', population: 79773 }, + { census_year: true, year: 1933, source: 'CoE', population: 79231 }, + { census_year: true, year: 1932, source: 'CoE', population: 78387 }, + { census_year: true, year: 1930, source: 'CoE', population: 77557 }, + { census_year: true, year: 1929, source: 'CoE', population: 74298 }, + { census_year: true, year: 1928, source: 'CoE', population: 69744 }, + { census_year: true, year: 1927, source: 'CoE', population: 67083 }, + { census_year: true, year: 1926, source: 'CoE', population: 65163 }, + { census_year: true, year: 1925, source: 'CoE', population: 65378 }, + { census_year: true, year: 1924, source: 'CoE', population: 63160 }, + { census_year: true, year: 1923, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1922, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1920, source: 'CoE', population: 61045 }, + { census_year: true, year: 1919, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1918, source: 'CoE*', population: 53000 }, + { census_year: true, year: 1917, source: 'CoE* 4', population: 51000 }, + { census_year: true, year: 1916, source: 'CoE', population: 53846 }, + { census_year: true, year: 1915, source: 'CoE', population: 59339 }, + { census_year: true, year: 1914, source: 'CoE', population: 72516 }, + { census_year: true, year: 1913, source: 'CoE', population: 67243 }, + { census_year: true, year: 1912, source: 'CoE 5', population: 53611 }, + { year: 1910, source: 'CoE 6', population: 23950 }, + { census_year: true, year: 1909, source: 'CoE*', population: 23000 }, + { census_year: true, year: 1908, source: 'CoE', population: 18500 }, + { year: 1907, source: 'CoE 6', population: 16294 }, + { census_year: true, year: 1906, source: 'CoE', population: 14088 }, + { year: 1905, source: 'CoE 6', population: 11219 }, + { census_year: true, year: 1904, source: 'CoE', population: 8350 }, + { census_year: true, year: 1903, source: 'CoE', population: 6995 }, + { year: 1902, source: 'CoE 6', population: 4811 }, + { year: 1900, source: 'CoE 6', population: 2419 }, + { census_year: true, year: 1899, source: 'CoE', population: 2212 }, + { year: 1898, source: 'CoE 6', population: 2212 }, + { census_year: true, year: 1897, source: 'CoE', population: 2212 }, + { year: 1896, source: 'CoE 6', population: 1689 }, + { census_year: true, year: 1895, source: 'CoE', population: 1165 }, + { year: 1894, source: 'CoE 6', population: 1010 }, + { year: 1893, source: 'CoE 6', population: 855 }, + { census_year: true, year: 1892, source: 'CoE', population: 700 }, + { year: 1891, source: 'CoE 6', population: 630 }, + { year: 1890, source: 'CoE 6', population: 560 }, + { year: 1889, source: 'CoE 6', population: 490 }, + { year: 1888, source: 'CoE 6', population: 420 }, + { census_year: true, year: 1887, source: 'CoE*', population: 350 }, + { year: 1886, source: 'CoE', population: 307 }, + { census_year: true, year: 1885, source: 'CoE', population: 263 }, + { year: 1884, source: 'CoE 6', population: 247 }, + { year: 1883, source: 'CoE 6', population: 230 }, + { year: 1882, source: 'CoE 6', population: 214 }, + { year: 1881, source: 'CoE 6', population: 197 }, + { year: 1880, source: 'CoE 6', population: 181 }, + { year: 1879, source: 'CoE 6', population: 164 }, + { census_year: true, year: 1878, source: 'CoE', population: 148 } + ].reverse() + } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-position.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-position.mjs new file mode 100644 index 0000000000..edccab754a --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/marker-richtext-position.mjs @@ -0,0 +1,210 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f0f52a1e9eec95f9ee2 + * 验证目的:起止点富文本标注的尺寸与偏移。 + * 保留条件:源数据、数组顺序、字段关联、回调及系列组合。 + * 迁移说明:仅适配宿主和已记录的通用文字;不修改原始数值。 + * 覆盖边界:仅验证静态最终状态,不把交互或动画配置视为行为已覆盖。 + */ +export default { + createSpec() { + // 每次重建独立源配置,保留确定性计算和回调。 + const spec = { + type: 'line', + xField: 'year', + yField: 'population', + axes: [{ orient: 'bottom', type: 'linear', min: 1868, max: 2016 }], + markPoint: [ + { + coordinate: { year: '1878', population: 100 }, + itemContent: { + offsetY: -100, + type: 'richText', + autoRotate: false, + richText: { + style: { + width: 100, + height: 50, + dx: -20, + dy: 5, + textConfig: [ + { text: '1878\n', fontWeight: 'bold', fontSize: 13, fill: '#E8346D', fontFamily: 'Times New Roman' }, + { text: 'population was 148', fontSize: 10, fill: '#E8346D', fontFamily: 'Times New Roman' } + ] + } + } + }, + itemLine: { + decorativeLine: { visible: true }, + startSymbol: { size: 20, style: { stroke: '#E8346D', fill: 'transparent', lineWidth: 1 } }, + line: { style: { stroke: '#E8346D' } } + } + }, + { + coordinate: { year: '2016', population: 899447 }, + itemContent: { + offsetY: 40, + offsetX: -200, + type: 'richText', + autoRotate: false, + position: 'insideBottom', + richText: { + style: { + width: 100, + height: 50, + dx: 110, + dy: 10, + textConfig: [ + { text: '2016\n', fontWeight: 'bold', fontSize: 13, fill: '#E8346D', fontFamily: 'Times New Roman' }, + { text: 'population was 899447', fontSize: 10, fill: '#E8346D', fontFamily: 'Times New Roman' } + ] + } + } + }, + itemLine: { + type: 'type-do', + startSymbol: { size: 24, style: { stroke: '#E8346D', fill: 'transparent', lineWidth: 1 } }, + line: { style: { stroke: '#E8346D' } } + } + } + ], + point: { style: { visible: false } }, + line: { style: { lineWidth: 2 } }, + title: { + text: 'Population History - Line Chart', + textStyle: { lineWidth: 3, fill: '#333', fontSize: 20, fontFamily: 'Times New Roman' } + }, + data: { + id: 'data2', + values: [ + { census_year: true, year: 2016, source: 'CoE', population: 899447 }, + { census_year: true, year: 2014, source: 'CoE', population: 877926 }, + { year: 2013, source: 'CoE 6', population: 847712 }, + { census_year: true, year: 2012, source: 'CoE', population: 817498 }, + { year: 2010, source: 'CoE 6', population: 797320 }, + { census_year: true, year: 2009, source: 'CoE', population: 782439 }, + { census_year: true, year: 2008, source: 'CoE', population: 752412 }, + { year: 2007, source: 'CoE 6', population: 741392 }, + { census_year: true, year: 2005, source: 'CoE', population: 712391 }, + { year: 2004, source: 'CoE 6', population: 698631 }, + { year: 2003, source: 'CoE 6', population: 684871 }, + { year: 2002, source: 'CoE 6', population: 671110 }, + { year: 2000, source: 'CoE 6', population: 652817 }, + { census_year: true, year: 1999, source: 'CoE', population: 648284 }, + { year: 1998, source: 'CoE 6', population: 637625 }, + { year: 1997, source: 'CoE 6', population: 626965 }, + { year: 1995, source: 'CoE 6', population: 619870 }, + { year: 1994, source: 'CoE 6', population: 623435 }, + { census_year: true, year: 1993, source: 'CoE', population: 626999 }, + { census_year: true, year: 1992, source: 'CoE', population: 618195 }, + { census_year: true, year: 1990, source: 'CoE', population: 605538 }, + { census_year: true, year: 1989, source: 'CoE', population: 583872 }, + { year: 1988, source: 'CoE', population: 580061 }, + { census_year: true, year: 1987, source: 'CoE', population: 576249 }, + { year: 1985, source: 'CoE 6', population: 567699 }, + { year: 1984, source: 'CoE 6', population: 563892 }, + { census_year: true, year: 1983, source: 'CoE', population: 560085 }, + { census_year: true, year: 1982, source: 'CoE 1', population: 551314 }, + { census_year: true, year: 1980, source: 'CoE', population: 505773 }, + { census_year: true, year: 1979, source: 'CoE', population: 491359 }, + { census_year: true, year: 1978, source: 'CoE', population: 478066 }, + { census_year: true, year: 1977, source: 'CoE', population: 471474 }, + { census_year: true, year: 1975, source: 'CoE', population: 451635 }, + { census_year: true, year: 1974, source: 'CoE', population: 445691 }, + { census_year: true, year: 1973, source: 'CoE', population: 442365 }, + { census_year: true, year: 1972, source: 'CoE', population: 441530 }, + { census_year: true, year: 1970, source: 'CoE', population: 429750 }, + { census_year: true, year: 1969, source: 'CoE', population: 422418 }, + { census_year: true, year: 1968, source: 'CoE', population: 410105 }, + { census_year: true, year: 1967, source: 'CoE', population: 393593 }, + { census_year: true, year: 1965, source: 'CoE', population: 371265 }, + { census_year: true, year: 1964, source: 'CoE 2', population: 311804 }, + { census_year: true, year: 1963, source: 'CoE', population: 303756 }, + { census_year: true, year: 1962, source: 'CoE', population: 294967 }, + { census_year: true, year: 1960, source: 'CoE', population: 269314 }, + { census_year: true, year: 1959, source: 'CoE', population: 260733 }, + { census_year: true, year: 1958, source: 'CoE', population: 252131 }, + { census_year: true, year: 1957, source: 'CoE', population: 238353 }, + { census_year: true, year: 1955, source: 'CoE', population: 209353 }, + { census_year: true, year: 1954, source: 'CoE', population: 197836 }, + { census_year: true, year: 1953, source: 'CoE', population: 183411 }, + { census_year: true, year: 1952, source: 'CoE', population: 169196 }, + { census_year: true, year: 1950, source: 'CoE', population: 148861 }, + { census_year: true, year: 1949, source: 'CoE', population: 137469 }, + { census_year: true, year: 1948, source: 'CoE', population: 126609 }, + { census_year: true, year: 1947, source: 'CoE', population: 118541 }, + { census_year: true, year: 1946, source: 'CoE', population: 114976 }, + { census_year: true, year: 1945, source: 'CoE', population: 111745 }, + { census_year: true, year: 1944, source: 'CoE', population: 108416 }, + { census_year: true, year: 1943, source: 'CoE', population: 105536 }, + { census_year: true, year: 1942, source: 'CoE', population: 96725 }, + { census_year: true, year: 1940, source: 'CoE', population: 91723 }, + { census_year: true, year: 1939, source: 'CoE', population: 90419 }, + { census_year: true, year: 1938, source: 'CoE', population: 88887 }, + { census_year: true, year: 1937, source: 'CoE', population: 87034 }, + { census_year: true, year: 1936, source: 'CoE', population: 85470 }, + { census_year: true, year: 1935, source: 'CoE', population: 82634 }, + { census_year: true, year: 1934, source: 'CoE', population: 79773 }, + { census_year: true, year: 1933, source: 'CoE', population: 79231 }, + { census_year: true, year: 1932, source: 'CoE', population: 78387 }, + { census_year: true, year: 1930, source: 'CoE', population: 77557 }, + { census_year: true, year: 1929, source: 'CoE', population: 74298 }, + { census_year: true, year: 1928, source: 'CoE', population: 69744 }, + { census_year: true, year: 1927, source: 'CoE', population: 67083 }, + { census_year: true, year: 1926, source: 'CoE', population: 65163 }, + { census_year: true, year: 1925, source: 'CoE', population: 65378 }, + { census_year: true, year: 1924, source: 'CoE', population: 63160 }, + { census_year: true, year: 1923, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1922, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1920, source: 'CoE', population: 61045 }, + { census_year: true, year: 1919, source: 'CoE*', population: 60000 }, + { census_year: true, year: 1918, source: 'CoE*', population: 53000 }, + { census_year: true, year: 1917, source: 'CoE* 4', population: 51000 }, + { census_year: true, year: 1916, source: 'CoE', population: 53846 }, + { census_year: true, year: 1915, source: 'CoE', population: 59339 }, + { census_year: true, year: 1914, source: 'CoE', population: 72516 }, + { census_year: true, year: 1913, source: 'CoE', population: 67243 }, + { census_year: true, year: 1912, source: 'CoE 5', population: 53611 }, + { year: 1910, source: 'CoE 6', population: 23950 }, + { census_year: true, year: 1909, source: 'CoE*', population: 23000 }, + { census_year: true, year: 1908, source: 'CoE', population: 18500 }, + { year: 1907, source: 'CoE 6', population: 16294 }, + { census_year: true, year: 1906, source: 'CoE', population: 14088 }, + { year: 1905, source: 'CoE 6', population: 11219 }, + { census_year: true, year: 1904, source: 'CoE', population: 8350 }, + { census_year: true, year: 1903, source: 'CoE', population: 6995 }, + { year: 1902, source: 'CoE 6', population: 4811 }, + { year: 1900, source: 'CoE 6', population: 2419 }, + { census_year: true, year: 1899, source: 'CoE', population: 2212 }, + { year: 1898, source: 'CoE 6', population: 2212 }, + { census_year: true, year: 1897, source: 'CoE', population: 2212 }, + { year: 1896, source: 'CoE 6', population: 1689 }, + { census_year: true, year: 1895, source: 'CoE', population: 1165 }, + { year: 1894, source: 'CoE 6', population: 1010 }, + { year: 1893, source: 'CoE 6', population: 855 }, + { census_year: true, year: 1892, source: 'CoE', population: 700 }, + { year: 1891, source: 'CoE 6', population: 630 }, + { year: 1890, source: 'CoE 6', population: 560 }, + { year: 1889, source: 'CoE 6', population: 490 }, + { year: 1888, source: 'CoE 6', population: 420 }, + { census_year: true, year: 1887, source: 'CoE*', population: 350 }, + { year: 1886, source: 'CoE', population: 307 }, + { census_year: true, year: 1885, source: 'CoE', population: 263 }, + { year: 1884, source: 'CoE 6', population: 247 }, + { year: 1883, source: 'CoE 6', population: 230 }, + { year: 1882, source: 'CoE 6', population: 214 }, + { year: 1881, source: 'CoE 6', population: 197 }, + { year: 1880, source: 'CoE 6', population: 181 }, + { year: 1879, source: 'CoE 6', population: 164 }, + { census_year: true, year: 1878, source: 'CoE', population: 148 } + ].reverse() + } + }; + return spec; + }, + async verify(page) { + // 核对完整输入与有效绘制;目标分支另外经过配置变异验证。 + await verifySourceSpec(page); + await verifyRendered(page); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-axis-marker.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-axis-marker.mjs new file mode 100644 index 0000000000..f99068bc3b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-axis-marker.mjs @@ -0,0 +1,370 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb765f97cc3d008de5b3fe + * 验证目的:轴标题标签及标注区域富文本。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const data1 = [ + { date: 'Day 1', workload: 7000 }, + { date: 'Day 2', workload: 1000 }, + { date: 'Day 3', workload: 6000 }, + { date: 'Day 4', workload: 4000 }, + { date: 'Day 5', workload: 8000 }, + { date: 'Day 6', workload: 3000 }, + { date: 'Day 7', workload: 9000 }, + { date: 'Day 8', workload: 2000 }, + { date: 'Day 9', workload: 5000 } + ]; + const richTextConfig = text => [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red' + }, + { + text: 'Alternative', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green' + } + ]; + const spec = { + type: 'bar', + height: 500, + markLine: [ + { + y: 5000, + label: { + visible: true, + position: 'end', + type: 'rich', + text: { + type: 'rich', + text: richTextConfig('Safe sugar intake 50g/day') + }, + style: { + textAlign: 'right', + textBaseline: 'bottom', + fill: '#000' + }, + labelBackground: { + visible: false + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + } + } + ], + markPoint: [ + { + coordinate: { + date: 'Day 3', + workload: 5000 + }, + itemContent: { + offsetY: 40, + offsetX: -200, + autoRotate: false, + text: { + type: 'rich', + text: [ + { + text: '111\n', + fontWeight: 'bold', + fontSize: 13, + fill: '#E8346D', + fontFamily: 'Times New Roman' + }, + { + text: 'population was 899447', + fontSize: 10, + fill: '#E8346D', + fontFamily: 'Times New Roman' + } + ], + style: { + textConfig: [ + { + text: '333\n', + fontWeight: 'bold', + fontSize: 13, + fill: '#E8346D', + fontFamily: 'Times New Roman' + }, + { + text: 'population was 899447', + fontSize: 10, + fill: '#E8346D', + fontFamily: 'Times New Roman' + } + ] + } + } + }, + itemLine: { + type: 'type-do', + startSymbol: { + size: 24, + style: { + stroke: '#E8346D' + } + }, + line: { + style: { + stroke: '#E8346D' + } + } + } + } + ], + markArea: [ + { + coordinates: [ + { + date: 'Day 1', + workload: 1000 + }, + { + date: 'Day 3', + workload: 1000 + }, + { + date: 'Day 3', + workload: 8000 + }, + { + date: 'Day 1', + workload: 8000 + } + ], + label: { + position: 'top', + type: 'rich', + text: { + type: 'rich', + text: [ + { + text: 'RICHTEXT', + fontWeight: 'bold', + fontSize: 25, + fill: '#3f51b5' + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ] + } + } + } + ], + padding: { + top: 24, + right: 48, + bottom: 48, + left: 48 + }, + data: [ + { + id: 'id0', + values: data1 + } + ], + label: { + visible: true, + formatMethod: (text, ...args) => { + if (!Array.isArray(text)) { + return { + type: 'rich', + text: [ + { + text: `${text}`, + fill: 'red', + fontSize: 20 + }, + { + image: + '', + width: 20, + height: 20 + } + ] + }; + } + return []; + } + }, + xField: 'date', + yField: 'workload', + axes: [ + { + orient: 'left', + label: { + visible: true, + space: 8, + style: { + fill: '#6F6F6F', + fontFamily: 'PingFang SC', + _debug_bounds: true + }, + formatMethod: text => { + return { + type: 'rich', + text: [ + { + text, + fontWeight: 'bold', + fontSize: 12, + fill: '#3f51b5' + }, + { + text: '🌞', + fill: '#3f51b5' + }, + { + text: '\nLargeLarge', + fill: 'red', + fontSize: 30 + } + ] + }; + } + }, + title: { + visible: true, + space: 20, + text: { + type: 'rich', + text: [ + { + text: 'text', + fontWeight: 'bold', + fontSize: 12, + fill: '#3f51b5' + }, + { + text: '🌞', + fill: '#3f51b5' + }, + { + text: '\nLargeLarge', + fill: 'red', + fontSize: 30 + } + ] + }, + autoRotate: false + } + }, + { + orient: 'bottom', + id: 'axis-bottom', + visible: true, + label: { + space: 8, + style: { + fill: '#6F6F6F', + fontFamily: 'PingFang SC', + _debug_bounds: true + }, + type: 'rich', + formatMethod: (text, datum) => { + return [ + { + text: `${text}`, + fill: 'black', + fontSize: 12 + }, + { + image: + '', + width: 12, + height: 12, + margin: [0, 0, 8, 4] + } + ]; + } + }, + title: { + visible: true, + space: 20, + text: 'value', + autoRotate: false, + style: { + dx: -100, + fill: '#333', + fontFamily: 'PingFang SC', + fontSize: 14, + fontWeight: 'bold', + textBaseline: 'bottom', + angle: -90 + } + } + } + ], + tooltip: { + enterable: true, + renderMode: 'canvas', + mark: { + title: { + value: { + type: 'rich', + text: [ + { + text: 'TOOLTIP', + fontWeight: 'bold', + fill: '#3f51b5' + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ] + } + } + } + }, + bar: { + style: { + fill: '#00924F' + }, + state: { + hover: { + fill: '#1664FF' + } + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-extension-mark.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-extension-mark.mjs new file mode 100644 index 0000000000..ebe4e16a88 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-extension-mark.mjs @@ -0,0 +1,110 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb785c97cc3d008de5b403 + * 验证目的:组合图自定义富文本图元。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'common', + data: [ + { + id: 'barData', + values: [ + { date: 'Day one', class: 'A', score: 20, highest: true }, + { date: 'Day one', class: 'B', score: 18 }, + { date: 'Day one', class: 'C', score: 19 }, + { date: 'Day one', class: 'D', score: 15 }, + { date: 'Day two', class: 'A', score: 16 }, + { date: 'Day two', class: 'B', score: 19, highest: true }, + { date: 'Day two', class: 'C', score: 18 }, + { date: 'Day two', class: 'D', score: 14 }, + { date: 'Day three', class: 'A', score: 19, highest: true }, + { date: 'Day three', class: 'B', score: 16 }, + { date: 'Day three', class: 'C', score: 18 }, + { date: 'Day three', class: 'D', score: 13 }, + { date: 'Day four', class: 'A', score: 18 }, + { date: 'Day four', class: 'B', score: 20 }, + { date: 'Day four', class: 'C', score: 22 }, + { date: 'Day four', class: 'D', score: 26, highest: true } + ] + } + ], + series: [ + { + type: 'bar', + yField: 'score', + xField: ['date', 'class'], + seriesField: 'class', + extensionMark: [ + { + type: 'text', + dataId: 'barData', + visible: true, + style: { + text: { + type: 'rich', + text: [ + { + text: 'TOOLTIP', + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5', + stroke: false + } + ] + }, + x: (datum, ctx, elements, dataView) => { + return ctx.valueToX([datum.date, datum.class]); + }, + y: (datum, ctx, elements, dataView) => { + return ctx.valueToY([datum.score]) - 28; + }, + fill: 'red', + visible: datum => datum.highest === true + } + } + ] + } + ], + title: { + visible: true, + text: 'Mark the class with the highest daily score with a red flag', + padding: { + left: 50, + bottom: 10 + } + }, + axes: [ + { orient: 'left', type: 'linear', nice: true, range: { max: 30 } }, + { orient: 'bottom', type: 'band' } + ] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-funnel-label.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-funnel-label.mjs new file mode 100644 index 0000000000..ce8c2e317c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-funnel-label.mjs @@ -0,0 +1,190 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb76d097cc3d008de5b401 + * 验证目的:漏斗内外标签富文本和装饰。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const richTextConfig = text => [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green', + stroke: false + } + ]; + const spec = { + type: 'funnel', + maxSize: '60%', + width: 600, + minSize: '10%', + height: 500, + isTransform: true, + shape: 'rect', + color: { + type: 'ordinal', + range: ['#00328E', '#0048AA', '#005FC5', '#2778E2', '#4E91FF', '#70ABFF', '#8FC7FF', '#AEE2FF'] + }, + funnel: { + style: { + cornerRadius: 4, + stroke: 'white', + lineWidth: 2 + }, + state: { + hover: { + stroke: '#4e83fd', + lineWidth: 1 + } + } + }, + transform: { + style: { + stroke: 'white', + lineWidth: 2 + }, + state: { + hover: { + stroke: '#4e83fd', + lineWidth: 1 + } + } + }, + outerLabel: { + visible: true, + alignLabel: false, + formatMethod: (text, datum) => { + if (datum && datum.value > 70) { + return { + type: 'rich', + text: [ + { + text: '1111', + fontWeight: 'bold', + fill: '#3f51b5', + stroke: false + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5', + stroke: false + } + ] + }; + } else { + return text; + } + } + }, + transformLabel: { + visible: true, + formatMethod: text => { + return { + type: 'rich', + text: [ + { + text, + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5', + stroke: false + } + ] + }; + } + }, + label: { + visible: true, + textType: 'rich', + formatMethod: () => { + return [ + { + text: 'TOOLTIP', + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5', + stroke: false + } + ]; + } + }, + data: [ + { + name: 'funnel', + values: [ + { + value: 100, + name: 'Resume Screening', + percent: 1 + }, + { + value: 80, + name: 'Resume Evaluation', + percent: 0.8 + }, + { + value: 50, + name: 'Evaluation Passed', + percent: 0.5 + }, + { + value: 30, + name: 'Interview', + percent: 0.3 + }, + { + value: 10, + name: 'Final Pass', + percent: 0.1 + } + ] + } + ], + categoryField: 'name', + valueField: 'value' + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-line-end-label.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-line-end-label.mjs new file mode 100644 index 0000000000..224327ac2d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-line-end-label.mjs @@ -0,0 +1,112 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb7a6f97cc3d008de5b40f + * 验证目的:折线末端富文本标签。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const richTextConfig = text => [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green', + stroke: false + } + ]; + const spec = { + type: 'line', + width: 800, + data: { + values: [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } + ] + }, + title: { + visible: true, + text: 'Stacked line chart' + }, + stack: true, + xField: 'type', + yField: 'value', + seriesField: 'country', + lineLabel: { visible: true, textType: 'rich', formatMethod: text => richTextConfig(text) }, + label: { + visible: true, + formatMethod: text => { + return { type: 'rich', text: richTextConfig(text) }; + } + }, + totalLabel: { + visible: true, + formatMethod: text => { + return { type: 'rich', text: richTextConfig(text) }; + } + }, + legends: [{ visible: true, position: 'middle', orient: 'bottom' }] + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-marker-label.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-marker-label.mjs new file mode 100644 index 0000000000..2d3e3ce3e1 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-marker-label.mjs @@ -0,0 +1,151 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 655ad3359ea0da0095642846 + * 验证目的:散点标注线富文本标签。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const spec = { + type: 'scatter', + padding: [12, 20, 12, 12], + xField: 'x', + yField: 'y', + sizeField: 'z', + size: { + type: 'linear', + range: [20, 80] + }, + axes: [ + { orient: 'bottom', type: 'linear', min: 60, max: 95 }, + { orient: 'left', type: 'linear', min: 0, max: 200 } + ], + point: { + style: { + fillOpacity: 0.25, + lineWidth: 1, + stroke: '#6690F2', + fill: '#6690F2' + } + }, + label: { + visible: true, + position: 'center', + overlap: { + avoidBaseMark: false + }, + style: { + stroke: '#fff', + lineWidth: 1 + } + }, + markLine: [ + { + x: 65, + label: { + visible: true, + position: 'end', + type: 'rich', + text: [ + { + text: 'RICHTEXT', + fontWeight: 'bold', + fontSize: 25, + fill: '#3f51b5' + }, + { + text: 'Alternative', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ], + style: { + textAlign: 'left', + textBaseline: 'top', + fill: '#000', + dx: 10 + }, + labelBackground: { + visible: false + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + } + }, + { + y: 50, + label: { + visible: true, + position: 'end', + text: 'Safe sugar intake 50g/day', + style: { + textAlign: 'right', + textBaseline: 'bottom', + fill: '#000' + }, + labelBackground: { + visible: false + } + }, + line: { + style: { + stroke: '#000', + lineDash: [0] + } + } + } + ], + tooltip: { + mark: { + title: { + value: datum => datum.country + } + } + }, + data: { + id: 'data', + values: [ + { x: 95, y: 95, z: 13.8, name: 'BE', country: 'Belgium' }, + { x: 86.5, y: 102.9, z: 14.7, name: 'DE', country: 'Germany' }, + { x: 80.8, y: 91.5, z: 15.8, name: 'FI', country: 'Finland' }, + { x: 80.4, y: 102.5, z: 12, name: 'NL', country: 'Netherlands' }, + { x: 80.3, y: 86.1, z: 11.8, name: 'SE', country: 'Sweden' }, + { x: 78.4, y: 70.1, z: 16.6, name: 'ES', country: 'Spain' }, + { x: 74.2, y: 68.5, z: 14.5, name: 'FR', country: 'France' }, + { x: 73.5, y: 83.1, z: 10, name: 'NO', country: 'Norway' }, + { x: 71, y: 93.2, z: 24.7, name: 'UK', country: 'United Kingdom' }, + { x: 69.2, y: 57.6, z: 10.4, name: 'IT', country: 'Italy' }, + { x: 68.6, y: 20, z: 16, name: 'RU', country: 'Russia' }, + { x: 65.5, y: 126.4, z: 35.3, name: 'US', country: 'United States' }, + { x: 65.4, y: 50.8, z: 28.5, name: 'HU', country: 'Hungary' }, + { x: 63.4, y: 51.8, z: 15.4, name: 'PT', country: 'Portugal' }, + { x: 64, y: 82.9, z: 31.3, name: 'NZ', country: 'New Zealand' } + ] + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-pie-title.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-pie-title.mjs new file mode 100644 index 0000000000..4bc4fd8387 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-pie-title.mjs @@ -0,0 +1,213 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb77ed97cc3d008de5b402 + * 验证目的:饼图富文本标题副标题及标签。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const richTextConfig = text => [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red' + }, + { + text: 'Alternative', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green' + } + ]; + const spec = { + type: 'pie', + data: [ + { + values: [ + { + value: '159', + type: 'Tradition Industries', + percentage: '71.6%' + }, + { + value: '50', + type: 'Business Companies', + percentage: '22.5%' + }, + { + value: '13', + type: 'Customer-facing Companies', + percentage: '5.9%' + } + ] + } + ], + title: { + visible: true, + align: 'left', + verticalAlign: 'top', + orient: 'top', + textType: 'rich', + text: [ + { + text: 'Chinese Character Test', + fontSize: 30, + textAlign: 'center', + textDecoration: 'underline', + stroke: '#0f51b5', + fill: false + } + ], + subtextType: 'rich', + subtext: [ + { + text: 'Mapbox', + fontWeight: 'bold', + fontSize: 30, + fill: '#3f51b5' + }, + { + text: 'was established in 2010 with the goal of providing an alternative solution', + fill: '#000' + }, + { + text: 'alternative solution', + fontStyle: 'italic', + fill: '#3f51b5' + }, + { + text: ' to Google Maps. At that time, Google Map', + fill: '#000' + }, + { + text: 'Map', + textDecoration: 'line-through', + fill: '#000' + }, + { + text: '[1]', + script: 'super', + fill: '#000' + }, + { + text: 'almost monopolized the online mapping business. However, within Google Maps, there was hardly any possibility for customization, and there were no tools available for map creators to create maps according to their own vision', + fill: '#000' + }, + { + text: '.\n', + fill: '#30ff05' + } + ] + }, + radius: 0.8, + innerRadius: 0.5, + valueField: 'value', + categoryField: 'type', + label: { + visible: true, + textType: 'rich', + formatMethod: text => richTextConfig(text), + style: { + fontSize: 16 + }, + line: { + style: {}, + line1MinLength: 30 + }, + layout: { + align: 'edge' + } + }, + pie: { + state: { + selected: { + outerRadius: 0.85 + } + } + }, + indicator: { + visible: true, + fixed: true, + gap: 10, + title: { + autoLimit: true, + style: { + fontSize: 16, + text: datum => { + if (datum && datum.value > 50) { + return 111; + } + return { + type: 'rich', + text: [ + { + text: 'type:', + fontWeight: 'bold', + fontSize: 20, + fill: '#3f51b5' + }, + { + text: datum ? datum.type : '', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ] + }; + } + } + }, + content: [ + { + style: { + fontSize: 42, + fontWeight: 'bolder', + type: 'rich', + text: [ + { + text: 'type:', + fontWeight: 'bold', + fontSize: 20, + fill: '#3f51b5' + }, + { + text: '11111', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ] + } + }, + { + field: 'percentage', + style: { + fontSize: 20 + } + } + ] + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/richtext/richtext-range-label.mjs b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-range-label.mjs new file mode 100644 index 0000000000..ec7aa05492 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/richtext/richtext-range-label.mjs @@ -0,0 +1,96 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 65bb79e297cc3d008de5b40e + * 验证目的:区间柱富文本标签。 + * 保留条件:保留源数据、确定性计算、回调及配置组合。 + * 迁移说明:仅适配宿主和通用文字;内联 SVG 使用原始路径,不依赖网络。 + * 覆盖边界:检查当前静态状态,不把交互配置或动画配置出现计作行为覆盖。 + */ +export default { + createSpec() { + // 重建原始声明与回调,保留数据关系和计算顺序。 + const richTextConfig = text => [ + { + text, + fontSize: 14, + fontWeight: 'bold', + fill: 'red', + stroke: false + }, + { + text: 'Alternative', + fontSize: 10, + lineThrough: true, + underline: true, + fill: 'green', + stroke: false + } + ]; + const spec = { + type: 'rangeColumn', + data: [ + { + values: [ + { type: 'Category One', min: 76, max: 100 }, + { type: 'Category Two', min: 56, max: 108 }, + { type: 'Category Three', min: 38, max: 129 }, + { type: 'Category Four', min: 58, max: 155 }, + { type: 'Category Five', min: 45, max: 120 }, + { type: 'Category Six', min: 23, max: 99 }, + { type: 'Category Seven', min: 18, max: 56 }, + { type: 'Category Eight', min: 18, max: 34 } + ] + } + ], + xField: 'type', + yField: ['min', 'max'], + label: { + visible: true, + formatMethod: (text, datum) => { + if (datum.min > 30) { + return { + type: 'rich', + text: richTextConfig(text) + }; + } + return 111; + }, + position: 'middle', + minLabel: { + visible: false, + formatMethod: text => { + return { + type: 'rich', + text: richTextConfig(text) + }; + } + }, + maxLabel: { + visible: true, + formatMethod: text => { + return { + type: 'rich', + text: richTextConfig(text) + }; + } + } + } + }; + + return spec; + }, + async verify(page) { + // 同浏览器重建期望配置,并要求目标内容实际绘制。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if ( + !window.__visualChart + .getStage() + .findAll(g => g.type === 'richtext' && g.attribute.visible !== false && g.globalAABBBounds.width() > 0, true) + .length + ) + throw new Error('富文本未实际绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-auto.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-auto.mjs new file mode 100644 index 0000000000..fbe0eeeb6b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-auto.mjs @@ -0,0 +1,141 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 64fe90861754d74d66e96291 + * 迁移边界:固定数据只有九类;先缩窄至 360×600,保留 bandSize:50 以产生真实溢出。 + * 验证目的:来源滚动条拖动与实际可视范围。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'area', + data: { + values: [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } + ] + }, + title: { visible: true, text: 'Stacked area chart of cosmetic products sales' }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [{ visible: true, position: 'middle', orient: 'bottom' }], + crosshair: { + xField: { visible: true, label: { visible: true } }, + yField: { visible: true, label: { visible: true } } + }, + axes: [ + { orient: 'bottom', type: 'band', bandSize: 50, sampling: false, label: { autoRotate: true } }, + { orient: 'left', type: 'linear' } + ], + scrollBar: [{ orient: 'bottom', start: 0, filterMode: 'axis', axisIndex: 0, auto: true }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const { cases, loadCase } = await import('./cases/index.mjs'); + const spec = ( + await loadCase(cases.find(c => c.id === new URL(location.href).searchParams.get('case'))) + ).createSpec(), + chartSpace = window.__visualChart; + window.__scrollEvents = []; + window.__scrollBefore = JSON.stringify( + chartSpace + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + chartSpace.on('scrollBarChange', e => window.__scrollEvents.push({ ...e.value })); + }); + await page.evaluate(() => window.__visualChart.resize(360, 600)); + await interactionFrame(page); + const p = await graphicCenter(page, 'slider'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 120, p.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate( + () => + (window.__scrollAfterDrag = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + )) + ); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart; + if (!window.__scrollEvents?.length) throw Error('滚动条未产生范围事件'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (window.__scrollAfterDrag === window.__scrollBefore) throw Error('滚动未改变绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-click-update.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-click-update.mjs new file mode 100644 index 0000000000..2ee13ec655 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-click-update.mjs @@ -0,0 +1,134 @@ +import { verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65e982facf6d2600b8a6f9a3 + * 迁移边界:滚动中间态与 updateSpec 后最终态分别断言;原始 spec 更新会恢复初始范围,不能要求最终几何仍保持滚动位置。 + * 验证目的:来源滚动条拖动后点击轴标签更新柱颜色。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { year: '2000', sales: 22 }, + { year: '2001', sales: 13 }, + { year: '2002', sales: 25 }, + { year: '2003', sales: 29 }, + { year: '2004', sales: 38 }, + { year: '2005', sales: 49 }, + { year: '2006', sales: 58 }, + { year: '2007', sales: 29 }, + { year: '2008', sales: 78 }, + { year: '2009', sales: 19 }, + { year: '2010', sales: 23 }, + { year: '2011', sales: 20 }, + { year: '2012', sales: 98 }, + { year: '2013', sales: 49 }, + { year: '2014', sales: 28 } + ] + } + ], + direction: 'horizontal', + yField: 'year', + xField: 'sales', + scrollBar: [{ orient: 'right', startValue: '2011', endValue: '2014', roam: true }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const { cases, loadCase } = await import('./cases/index.mjs'); + const spec = ( + await loadCase(cases.find(c => c.id === new URL(location.href).searchParams.get('case'))) + ).createSpec(), + chartSpace = window.__visualChart; + chartSpace.on('click', { nodeName: 'axis-label' }, e => { + chartSpace.updateSpec({ ...spec, bar: { style: { fill: 'red' } } }); + }); + window.__scrollEvents = []; + window.__scrollBefore = JSON.stringify( + chartSpace + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + chartSpace.on('scrollBarChange', e => window.__scrollEvents.push({ ...e.value })); + }); + const p = await graphicCenter(page, 'slider'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 0, p.y + 120, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate( + () => + (window.__scrollAfterDrag = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + )) + ); + const label = await page.evaluate(() => { + const g = window.__visualChart + .getStage() + .findAll(g => g.name === 'axis-label' && g.globalAABBBounds.y1 > 20 && g.globalAABBBounds.y2 < 570, true) + .find(g => g.attribute.pickable !== false); + if (!g) throw Error('缺少可点击轴标签'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(label.x, label.y); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart; + if (!window.__scrollEvents?.length) throw Error('滚动条未产生范围事件'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (window.__scrollAfterDrag === window.__scrollBefore) throw Error('滚动未改变绘制'); + if ( + c.getSpec().bar?.style?.fill !== 'red' || + !c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .every(g => g.attribute.fill === 'red') + ) + throw Error('轴标签点击没有更新颜色'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-range.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-range.mjs new file mode 100644 index 0000000000..55e32649b6 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-axis-range.mjs @@ -0,0 +1,107 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 65548e5eb53f99008e8e6db2 + * 验证目的:隐藏滚动条的初始范围及分类轴显示。 + * 保留条件:xField, yField, scrollBar, crosshair;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + year: '2000', + sales: 22 + }, + { + year: '2001', + sales: 13 + }, + { + year: '2002', + sales: 25 + }, + { + year: '2003', + sales: 29 + }, + { + year: '2004', + sales: 38 + }, + { + year: '2005', + sales: 49 + }, + { + year: '2006', + sales: 58 + }, + { + year: '2007', + sales: 29 + }, + { + year: '2008', + sales: 78 + }, + { + year: '2009', + sales: 19 + }, + { + year: '2010', + sales: 23 + }, + { + year: '2011', + sales: 20 + }, + { + year: '2012', + sales: 98 + }, + { + year: '2013', + sales: 49 + }, + { + year: '2014', + sales: 28 + } + ] + } + ], + xField: 'year', + yField: 'sales', + scrollBar: [ + { + orient: 'bottom', + start: 0, + end: 0.5, + roamZoom: { + enable: true + }, + visible: false + } + ], + crosshair: { + xField: { + visible: false + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-crosshair.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-crosshair.mjs new file mode 100644 index 0000000000..47c08fbefc --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-crosshair.mjs @@ -0,0 +1,141 @@ +import { verifySourceSpec, verifyRendered, graphicCenter } from '../../../helpers.mjs'; +import { interactionFrame } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 655cadc3ba621bd56533f005 + * 迁移边界:缩窄至 360×600 触发自动滚动;当前仅验证滚动和逆序轴,不宣称已验证拖动后的十字线。 + * 验证目的:来源滚动条拖动与实际可视范围。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'area', + data: { + values: [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } + ] + }, + title: { visible: true, text: 'Stacked area chart of cosmetic products sales' }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [{ visible: true, position: 'middle', orient: 'bottom' }], + crosshair: { + xField: { visible: true, label: { visible: true } }, + yField: { visible: true, label: { visible: true } } + }, + axes: [ + { orient: 'bottom', type: 'band', bandSize: 50, sampling: false, inverse: true, label: { autoRotate: true } }, + { orient: 'left', type: 'linear' } + ], + scrollBar: [{ orient: 'bottom', start: 0, filterMode: 'axis', axisIndex: 0, auto: true }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const { cases, loadCase } = await import('./cases/index.mjs'); + const spec = ( + await loadCase(cases.find(c => c.id === new URL(location.href).searchParams.get('case'))) + ).createSpec(), + chartSpace = window.__visualChart; + window.__scrollEvents = []; + window.__scrollBefore = JSON.stringify( + chartSpace + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + chartSpace.on('scrollBarChange', e => window.__scrollEvents.push({ ...e.value })); + }); + await page.evaluate(() => window.__visualChart.resize(360, 600)); + await interactionFrame(page); + const p = await graphicCenter(page, 'slider'); + await page.mouse.move(p.x, p.y); + await page.mouse.down(); + await page.mouse.move(p.x + 120, p.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate( + () => + (window.__scrollAfterDrag = JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + )) + ); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart; + if (!window.__scrollEvents?.length) throw Error('滚动条未产生范围事件'); + const actual = JSON.stringify( + c + .getChart() + .getAllSeries() + .map(s => + s + .getSeriesMark() + .getGraphics() + .map(g => [g.attribute.x, g.attribute.y, g.attribute.width, g.attribute.height, g.attribute.points]) + ) + ); + if (window.__scrollAfterDrag === window.__scrollBefore) throw Error('滚动未改变绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-axis.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-axis.mjs new file mode 100644 index 0000000000..515190968c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-axis.mjs @@ -0,0 +1,267 @@ +import { verifySpec } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f1d52a1e9eec95f9f06 + * 验证目的:拖动滚动条调整轴范围。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'line', + data: { + values: [ + { + time: '0:05', + value: 10 + }, + { + time: '0:10', + value: 18 + }, + { + time: '0:15', + value: 20 + }, + { + time: '0:20', + value: 18 + }, + { + time: '0:25', + value: 20 + }, + { + time: '0:30', + value: 18 + }, + { + time: '0:35', + value: 20 + }, + { + time: '0:40', + value: 18 + }, + { + time: '0:45', + value: 20 + }, + { + time: '0:50', + value: 18 + }, + { + time: '0:55', + value: 10 + }, + { + time: '1:00', + value: 28 + }, + { + time: '1:05', + value: 18 + }, + { + time: '1:10', + value: 14 + }, + { + time: '1:15', + value: 12 + }, + { + time: '1:20', + value: 9 + }, + { + time: '1:25', + value: 20 + }, + { + time: '1:30', + value: 3 + }, + { + time: '1:35', + value: 4 + }, + { + time: '1:40', + value: 5 + }, + { + time: '1:45', + value: 10 + }, + { + time: '1:50', + value: 16 + }, + { + time: '1:55', + value: 10 + }, + { + time: '2:00', + value: 8 + }, + { + time: '2:05', + value: 18 + }, + { + time: '2:10', + value: 14 + }, + { + time: '2:15', + value: 12 + }, + { + time: '2:20', + value: 9 + }, + { + time: '2:25', + value: 20 + }, + { + time: '2:30', + value: 3 + }, + { + time: '2:35', + value: 4 + }, + { + time: '2:40', + value: 5 + }, + { + time: '2:45', + value: 10 + }, + { + time: '2:50', + value: 16 + }, + { + time: '2:55', + value: 10 + }, + { + time: '4:00', + value: 9 + }, + { + time: '6:00', + value: 11 + }, + { + time: '8:00', + value: 14 + }, + { + time: '10:00', + value: 16 + }, + { + time: '12:00', + value: 17 + }, + { + time: '14:00', + value: 17 + }, + { + time: '16:00', + value: 16 + }, + { + time: '18:00', + value: 15 + } + ] + }, + xField: 'time', + yField: 'value', + scrollBar: [ + { + orient: 'bottom', + start: 0.8, + end: 1, + roam: true, + filterMode: 'axis' + } + ] + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + await page.evaluate(() => { + const c = window.__visualChart; + window.__scrollBefore = { + spec: structuredClone(c.getSpec().scrollBar[0]), + geometry: JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + }; + c.on('scrollBarChange', e => { + window.__scrollResult = e.value; + }); + }); + const geometry = await page.evaluate(() => { + const g = window.__visualChart.getStage().find(g => g.name === 'slider', true); + if (!g) throw Error('缺少滚动条手柄'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(geometry.x, geometry.y); + await page.mouse.down(); + await page.mouse.move(geometry.x - 120, geometry.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const before = window.__scrollBefore, + after = window.__scrollResult; + if (!before || !after) return false; + const moved = + (Number.isFinite(before.spec.start) && Math.abs(before.spec.start - after.start) > 0.01) || + (Number.isFinite(before.spec.end) && Math.abs(before.spec.end - after.end) > 0.01) || + (before.spec.startValue !== undefined && before.spec.startValue !== after.startValue) || + (before.spec.endValue !== undefined && before.spec.endValue !== after.endValue); + const graphics = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + return ( + moved && + before.geometry !== + JSON.stringify( + graphics.map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + ); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-vertical.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-vertical.mjs new file mode 100644 index 0000000000..71defce145 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-vertical.mjs @@ -0,0 +1,158 @@ +import { verifySpec } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f1d52a1e9eec95f9f07 + * 验证目的:拖动纵向滚动条改变范围。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + year: '2000', + sales: 22 + }, + { + year: '2001', + sales: 13 + }, + { + year: '2002', + sales: 25 + }, + { + year: '2003', + sales: 29 + }, + { + year: '2004', + sales: 38 + }, + { + year: '2005', + sales: 49 + }, + { + year: '2006', + sales: 58 + }, + { + year: '2007', + sales: 29 + }, + { + year: '2008', + sales: 78 + }, + { + year: '2009', + sales: 19 + }, + { + year: '2010', + sales: 23 + }, + { + year: '2011', + sales: 20 + }, + { + year: '2012', + sales: 98 + }, + { + year: '2013', + sales: 49 + }, + { + year: '2014', + sales: 28 + } + ] + } + ], + direction: 'horizontal', + yField: 'year', + xField: 'sales', + scrollBar: [ + { + orient: 'right', + startValue: '2011', + endValue: '2014', + roam: true + } + ] + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + await page.evaluate(() => { + const c = window.__visualChart; + window.__scrollBefore = { + spec: structuredClone(c.getSpec().scrollBar[0]), + geometry: JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + }; + c.on('scrollBarChange', e => { + window.__scrollResult = e.value; + }); + }); + const geometry = await page.evaluate(() => { + const g = window.__visualChart.getStage().find(g => g.name === 'slider', true); + if (!g) throw Error('缺少滚动条手柄'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(geometry.x, geometry.y); + await page.mouse.down(); + await page.mouse.move(geometry.x + 0, geometry.y + 120, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const before = window.__scrollBefore, + after = window.__scrollResult; + if (!before || !after) return false; + const moved = + (Number.isFinite(before.spec.start) && Math.abs(before.spec.start - after.start) > 0.01) || + (Number.isFinite(before.spec.end) && Math.abs(before.spec.end - after.end) > 0.01) || + (before.spec.startValue !== undefined && before.spec.startValue !== after.startValue) || + (before.spec.endValue !== undefined && before.spec.endValue !== after.endValue); + const graphics = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + return ( + moved && + before.geometry !== + JSON.stringify( + graphics.map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + ); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-window.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-window.mjs new file mode 100644 index 0000000000..5d003ffa5b --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-drag-window.mjs @@ -0,0 +1,157 @@ +import { verifySpec } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f1c52a1e9eec95f9f05 + * 验证目的:拖动横向滚动条改变可视窗口。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + year: '2000', + sales: 22 + }, + { + year: '2001', + sales: 13 + }, + { + year: '2002', + sales: 25 + }, + { + year: '2003', + sales: 29 + }, + { + year: '2004', + sales: 38 + }, + { + year: '2005', + sales: 49 + }, + { + year: '2006', + sales: 58 + }, + { + year: '2007', + sales: 29 + }, + { + year: '2008', + sales: 78 + }, + { + year: '2009', + sales: 19 + }, + { + year: '2010', + sales: 23 + }, + { + year: '2011', + sales: 20 + }, + { + year: '2012', + sales: 98 + }, + { + year: '2013', + sales: 49 + }, + { + year: '2014', + sales: 28 + } + ] + } + ], + xField: 'year', + yField: 'sales', + scrollBar: [ + { + orient: 'bottom', + start: 0, + end: 0.5, + roam: true + } + ] + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + await page.evaluate(() => { + const c = window.__visualChart; + window.__scrollBefore = { + spec: structuredClone(c.getSpec().scrollBar[0]), + geometry: JSON.stringify( + c + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + }; + c.on('scrollBarChange', e => { + window.__scrollResult = e.value; + }); + }); + const geometry = await page.evaluate(() => { + const g = window.__visualChart.getStage().find(g => g.name === 'slider', true); + if (!g) throw Error('缺少滚动条手柄'); + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(geometry.x, geometry.y); + await page.mouse.down(); + await page.mouse.move(geometry.x + 120, geometry.y + 0, { steps: 12 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const before = window.__scrollBefore, + after = window.__scrollResult; + if (!before || !after) return false; + const moved = + (Number.isFinite(before.spec.start) && Math.abs(before.spec.start - after.start) > 0.01) || + (Number.isFinite(before.spec.end) && Math.abs(before.spec.end - after.end) > 0.01) || + (before.spec.startValue !== undefined && before.spec.startValue !== after.startValue) || + (before.spec.endValue !== undefined && before.spec.endValue !== after.endValue); + const graphics = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + return ( + moved && + before.geometry !== + JSON.stringify( + graphics.map(g => [ + g.globalAABBBounds.x1, + g.globalAABBBounds.y1, + g.globalAABBBounds.x2, + g.globalAABBBounds.y2, + g.attribute.points + ]) + ) + ); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-range-mode.mjs b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-range-mode.mjs new file mode 100644 index 0000000000..bf93bf2cfb --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/scrollbar/scrollbar-range-mode.mjs @@ -0,0 +1,100 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 66d527361ea06200dae97791 + * 验证目的:滚动条值和比例混合范围。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + year: '2000', + sales: 22 + }, + { + year: '2001', + sales: 13 + }, + { + year: '2002', + sales: 25 + }, + { + year: '2003', + sales: 29 + }, + { + year: '2004', + sales: 38 + }, + { + year: '2005', + sales: 49 + }, + { + year: '2006', + sales: 58 + }, + { + year: '2007', + sales: 29 + }, + { + year: '2008', + sales: 78 + }, + { + year: '2009', + sales: 19 + }, + { + year: '2010', + sales: 23 + }, + { + year: '2011', + sales: 20 + }, + { + year: '2012', + sales: 98 + }, + { + year: '2013', + sales: 49 + }, + { + year: '2014', + sales: 28 + } + ] + } + ], + direction: 'horizontal', + yField: 'year', + xField: 'sales', + scrollBar: [ + { + orient: 'right', + rangeMode: ['percent', 'value'], + start: 0, + endValue: '2006', + roam: true + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/title/bar-title.mjs b/packages/vchart/__tests__/visual/cases/components/title/bar-title.mjs new file mode 100644 index 0000000000..9496ac1184 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/title/bar-title.mjs @@ -0,0 +1,58 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; + +/** + * BugServer case IDs: 69d75d25943f32005d412309 + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:基础柱图显示主标题。 + * 保留条件:四个原始月份和值、data 数组、可见主标题;原文无副标题。 + * 改写说明:仅替换说明中提及的通用类目或标题文本;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'bar', + data: [ + { + id: 'id0', + values: [ + { + month: 'Jan', + value: 32 + }, + { + month: 'Feb', + value: 41 + }, + { + month: 'Mar', + value: 36 + }, + { + month: 'Apr', + value: 54 + } + ] + } + ], + xField: 'month', + yField: 'value', + title: { + visible: true, + text: 'VChart Demo' + }, + animation: false + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'title'); + await page.evaluate(() => { + const series = window.__visualChart.getChart().getAllSeries(); + if (series.length !== 1 || series[0].getSeriesMark().getGraphics().length !== 4) + throw new Error('主标题场景的四根柱图未完整绘制'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/axis-richtext-tooltip.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/axis-richtext-tooltip.mjs new file mode 100644 index 0000000000..566029a307 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/axis-richtext-tooltip.mjs @@ -0,0 +1,261 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 655ad0e39ea0da009564283b + * 验证目的:来源 Tooltip 配置、实际提示内容及移出隐藏。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data1 = [ + { date: 'Day 1', workload: 7000 }, + { date: 'Day 2', workload: 1000 }, + { date: 'Day 3', workload: 6000 }, + { date: 'Day 4', workload: 4000 }, + { date: 'Day 5', workload: 8000 }, + { date: 'Day 6', workload: 3000 }, + { date: 'Day 7', workload: 9000 }, + { date: 'Day 8', workload: 2000 }, + { date: 'Day 9', workload: 5000 } + ]; + const spec = { + type: 'bar', + padding: { top: 24, right: 48, bottom: 48, left: 48 }, + data: [{ id: 'id0', values: data1 }], + xField: 'date', + yField: 'workload', + axes: [ + { + orient: 'bottom', + label: { + space: 8, + style: { fill: '#505050', fontFamily: 'PingFang SC', angle: 0 }, + type: 'rich', + formatMethod: text => { + return [ + { text: text, fontWeight: 'bold', fontSize: 12, fill: '#3f51b5' }, + { text: '🌞', fill: '#3f51b5' } + ]; + } + }, + tick: { visible: false }, + title: { + visible: true, + space: 20, + style: { fill: '#333', fontFamily: 'PingFang SC', fontSize: 14, fontWeight: 'bold' }, + text: { + type: 'rich', + text: [ + { text: 'Date', fontWeight: 'bold', fontSize: 25, fill: '#3f51b5' }, + { text: '日期', fontStyle: 'italic', textDecoration: 'underline', fill: '#3f51b5' } + ] + } + } + }, + { + orient: 'left', + label: { + space: 8, + style: { fill: '#6F6F6F', fontFamily: 'PingFang SC' }, + formatMethod: label => `${label}K` + }, + grid: { + visible: true, + style: { + lineDash: [0] + } + }, + tick: { + visible: false + }, + title: { + visible: true, + space: 20, + text: 'value', + autoRotate: false, + style: { + fill: '#333', + fontFamily: 'PingFang SC', + fontSize: 14, + fontWeight: 'bold', + textBaseline: 'bottom', + angle: -90 + } + } + } + ], + dataZoom: { + orient: 'bottom', + showDetail: true, + middleHandler: { + visible: true + }, + backgroundChart: { + area: { + style: { + fill: '#EAEAEA', + fillOpacity: 0.5 + } + }, + line: { + style: { + stroke: '#EAEAEA', + lineWidth: 3 + } + } + }, + selectedBackgroundChart: { + area: { + style: { + fill: '#EAEAEA' + } + }, + line: { + style: { + stroke: '#EAEAEA', + lineWidth: 1 + } + } + }, + background: { + style: { + fill: '#fff', + lineWidth: 1, + stroke: '#EAEAEA' + } + }, + selectedBackground: { + style: { + fillOpacity: 0.1 + } + } + }, + crosshair: { + xField: { + visible: true, + label: { + visible: false + } + }, + yField: { + visible: false + } + }, + tooltip: { + enterable: true, + renderMode: 'canvas', + mark: { + title: { + value: { + type: 'rich', + text: [ + { + text: 'TOOLTIP', + fontWeight: 'bold', + fill: '#3f51b5' + }, + { + text: '替代方案', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ] + } + } + } + }, + bar: { + style: { + fill: '#00924F' + }, + state: { + hover: { + fill: '#1664FF' + } + } + }, + title: { + textType: 'rich', + text: [ + { + text: 'RICHTEXT', + fontWeight: 'bold', + fontSize: 25, + fill: '#3f51b5', + stroke: false + }, + { + text: '替代方案', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5', + stroke: false + } + ] + }, + label: { + visible: true, + textType: 'rich', + formatMethod: text => { + return [ + { + text: text + '', + fontWeight: 'bold', + fontSize: 12, + fill: '#3f51b5' + }, + { + text: 'K', + fontStyle: 'italic', + textDecoration: 'underline', + fill: '#3f51b5' + } + ]; + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'bar'); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + const first = await page.evaluate(() => { + const c = window.__visualChart; + const texts = c + .getStage() + .findAll(g => g.name?.includes('tooltip'), true) + .flatMap(g => g.findAll?.(g => g.type === 'text' || g.type === 'richtext', true) ?? []) + .map(g => JSON.stringify(g.attribute.textConfig ?? g.attribute.text)); + return ( + [...document.querySelectorAll('[class*="tooltip"]')] + .filter(el => el.getBoundingClientRect().width > 0 && getComputedStyle(el).visibility !== 'hidden') + .map(el => el.textContent) + .join(' ') + texts.join(' ') + ); + }); + await page.mouse.move(950, 750); + await page.waitForFunction(() => !window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.evaluate(first => (window.__tooltipText = first), first); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (!window.__tooltipText?.trim() || !window.__visualChart.getTooltipHandler()?.isTooltipShown()) + throw Error('提示未显示有效内容或未恢复'); + }); + await page.evaluate(() => { + if (!window.__tooltipText.includes('TOOLTIP') || !window.__tooltipText.includes('替代方案')) + throw Error('富文本 Tooltip 内容缺失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-click.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-click.mjs new file mode 100644 index 0000000000..4a24799847 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-click.mjs @@ -0,0 +1,195 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e038d + * 验证目的:点击触发Canvas提示框。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '860' + }, + { + x: '1', + type: 'A', + y: '721' + }, + { + x: '2', + type: 'A', + y: '653' + }, + { + x: '3', + type: 'A', + y: '726' + }, + { + x: '4', + type: 'A', + y: '645' + }, + { + x: '5', + type: 'A', + y: '853' + }, + { + x: '6', + type: 'A', + y: '602' + }, + { + x: '7', + type: 'A', + y: '810' + }, + { + x: '8', + type: 'A', + y: '879' + }, + { + x: '9', + type: 'A', + y: '837' + }, + { + x: '0', + type: 'B', + y: '816' + }, + { + x: '1', + type: 'B', + y: '608' + }, + { + x: '2', + type: 'B', + y: '787' + }, + { + x: '3', + type: 'B', + y: '704' + }, + { + x: '4', + type: 'B', + y: '791' + }, + { + x: '5', + type: 'B', + y: '849' + }, + { + x: '6', + type: 'B', + y: '893' + }, + { + x: '7', + type: 'B', + y: '686' + }, + { + x: '8', + type: 'B', + y: '638' + }, + { + x: '9', + type: 'B', + y: '810' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6 + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + tooltip: { + visible: true, + renderMode: 'canvas', + trigger: 'click' + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-hover.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-hover.mjs new file mode 100644 index 0000000000..92ffc6b9b8 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-canvas-hover.mjs @@ -0,0 +1,86 @@ +import { interactionTarget, interactionFrame } from '../../../interaction-helpers.mjs'; +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03f5 + * 验证目的:悬停柱形显示Canvas提示框。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + renderMode: 'canvas' + } + }; + }, + async exercise(page) { + // 保留多次 move 的语义:不同 datum 的提示、移出隐藏、恢复最终提示。 + const observations = []; + for (const index of [0, 1, -1, 1]) { + let expected = ''; + if (index >= 0) { + const p = await interactionTarget(page, 'bar', index); + expected = await page.evaluate(index => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + return String(s.getViewData().latestData[index][s.getSpec().yField]); + }, index); + await page.mouse.move(p.x, p.y); + } else await page.mouse.move(950, 750); + await page.waitForFunction( + shown => window.__visualChart.getTooltipHandler()?.isTooltipShown() === shown, + index >= 0 + ); + await interactionFrame(page); + observations.push( + await page.evaluate(expected => { + const c = window.__visualChart, + roots = c.getStage().findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + const text = roots + .flatMap(g => g.findAll?.(g => g.type === 'text', true) ?? []) + .map(g => String(g.attribute.text)) + .join(' '); + return { shown: c.getTooltipHandler()?.isTooltipShown(), expected, text }; + }, expected) + ); + } + await page.evaluate(observations => { + window.__tooltipObservations = observations; + }, observations); + }, + async verify(page) { + // Canvas 提示必须包含对应 datum 的值,切换和隐藏均以实际组件状态为准。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.evaluate(() => { + const rows = window.__tooltipObservations; + if (rows?.length !== 4) throw Error('缺少多步 Tooltip 结果'); + for (const i of [0, 1, 3]) + if (!rows[i].shown || !rows[i].text.includes(rows[i].expected)) throw Error('Canvas Tooltip 内容不匹配'); + if (rows[2].shown || rows[0].expected === rows[1].expected || rows[0].text === rows[1].text) + throw Error('Tooltip 未切换或隐藏'); + if (!window.__visualChart.getTooltipHandler()?.isTooltipShown()) throw Error('最终 Tooltip 未恢复'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-click.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-click.mjs new file mode 100644 index 0000000000..9fc9bbca37 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-click.mjs @@ -0,0 +1,194 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e038a + * 验证目的:点击触发HTML提示框。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '860' + }, + { + x: '1', + type: 'A', + y: '721' + }, + { + x: '2', + type: 'A', + y: '653' + }, + { + x: '3', + type: 'A', + y: '726' + }, + { + x: '4', + type: 'A', + y: '645' + }, + { + x: '5', + type: 'A', + y: '853' + }, + { + x: '6', + type: 'A', + y: '602' + }, + { + x: '7', + type: 'A', + y: '810' + }, + { + x: '8', + type: 'A', + y: '879' + }, + { + x: '9', + type: 'A', + y: '837' + }, + { + x: '0', + type: 'B', + y: '816' + }, + { + x: '1', + type: 'B', + y: '608' + }, + { + x: '2', + type: 'B', + y: '787' + }, + { + x: '3', + type: 'B', + y: '704' + }, + { + x: '4', + type: 'B', + y: '791' + }, + { + x: '5', + type: 'B', + y: '849' + }, + { + x: '6', + type: 'B', + y: '893' + }, + { + x: '7', + type: 'B', + y: '686' + }, + { + x: '8', + type: 'B', + y: '638' + }, + { + x: '9', + type: 'B', + y: '810' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6 + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + tooltip: { + visible: true, + trigger: 'click' + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-content-reduce.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-content-reduce.mjs new file mode 100644 index 0000000000..4f52ff72be --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-content-reduce.mjs @@ -0,0 +1,120 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7f2352a1e9eec95f9f17 + * 验证目的:来源 Tooltip 配置、实际提示内容及移出隐藏。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const data = [ + { x: '2:00', y: 82 }, + { x: '4:00', y: 50 }, + { x: '6:00', y: 64 }, + { x: '8:00', y: 10 }, + { x: '10:00', y: 30 }, + { x: '12:00', y: 40 }, + { x: '14:00', y: 56 }, + { x: '16:00', y: 40 }, + { x: '18:00', y: 64 }, + { x: '20:00', y: 74 }, + { x: '22:00', y: 98 } + ]; + const sum = data.reduce((sum, cur) => sum + cur.y, 0); + const spec = { + type: 'bar', + data: { id: 'data1', values: data }, + xField: 'x', + yField: 'y', + bar: { + style: { + fill: { + gradient: 'linear', + x0: 0.5, + y0: 0.4, + x1: 1, + y1: 0.5, + stops: [ + { offset: 0, color: '#4FC6B4' }, + { offset: 1, color: '#31679E' } + ] + }, + cornerRadius: 10 + } + }, + tooltip: { + transitionDuration: 0, + mark: { + position: 'top', + content: [ + { key: '指标', value: datum => datum.y }, + { key: '占比', value: datum => Math.floor((datum.y / sum) * 10000) / 100 + '%', hasShape: false } + ], + updateContent: prev => + (prev ?? []) + .map(c => { + c.key += '的值是'; + return c; + }) + .concat({ key: '我是', value: '新加的一行' }) + }, + style: { + panel: { + padding: { top: 5, bottom: 10, left: 10, right: 10 }, + backgroundColor: '#272d54', + border: { color: '#999', width: 4, radius: 10 }, + shadow: { x: 0, y: 0, blur: 10, spread: 5, color: '#666' } + }, + titleLabel: { fontSize: 20, fontColor: 'white', fontWeight: 'bold', align: 'center', lineHeight: 24 }, + keyLabel: { fontSize: 14, fontColor: 'orange', align: 'center', lineHeight: 15, spacing: 10 }, + valueLabel: { fontSize: 14, fontColor: 'yellow', align: 'center', lineHeight: 15, spacing: 10 }, + shape: { size: 15, spacing: 10 }, + spaceRow: 10 + }, + offset: { y: 20 } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'bar'); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + const first = await page.evaluate(() => { + const c = window.__visualChart; + const texts = c + .getStage() + .findAll(g => g.name?.includes('tooltip'), true) + .flatMap(g => g.findAll?.(g => g.type === 'text' || g.type === 'richtext', true) ?? []) + .map(g => JSON.stringify(g.attribute.textConfig ?? g.attribute.text)); + return ( + [...document.querySelectorAll('[class*="tooltip"]')] + .filter(el => el.getBoundingClientRect().width > 0 && getComputedStyle(el).visibility !== 'hidden') + .map(el => el.textContent) + .join(' ') + texts.join(' ') + ); + }); + await page.mouse.move(950, 750); + await page.waitForFunction(() => !window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.evaluate(first => (window.__tooltipText = first), first); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (!window.__tooltipText?.trim() || !window.__visualChart.getTooltipHandler()?.isTooltipShown()) + throw Error('提示未显示有效内容或未恢复'); + }); + await page.evaluate(() => { + for (const text of ['指标的值是', '占比的值是', '新加的一行']) + if (!window.__tooltipText.includes(text)) throw Error('updateContent 缺少 ' + text); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-dimension-style.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-dimension-style.mjs new file mode 100644 index 0000000000..fed9f74cb8 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-dimension-style.mjs @@ -0,0 +1,128 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 64dc73f1ae69d5156f811c0a + * 验证目的:来源 Tooltip 配置、实际提示内容及移出隐藏。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'area', + data: { + values: [ + { type: 'Nail polish', country: 'Africa', value: 4229 }, + { type: 'Nail polish', country: 'EU', value: 4376 }, + { type: 'Nail polish', country: 'China', value: 3054 }, + { type: 'Nail polish', country: 'USA', value: 12814 }, + { type: 'Eyebrow pencil', country: 'Africa', value: 3932 }, + { type: 'Eyebrow pencil', country: 'EU', value: 3987 }, + { type: 'Eyebrow pencil', country: 'China', value: 5067 }, + { type: 'Eyebrow pencil', country: 'USA', value: 13012 }, + { type: 'Rouge', country: 'Africa', value: 5221 }, + { type: 'Rouge', country: 'EU', value: 3574 }, + { type: 'Rouge', country: 'China', value: 7004 }, + { type: 'Rouge', country: 'USA', value: 11624 }, + { type: 'Lipstick', country: 'Africa', value: 9256 }, + { type: 'Lipstick', country: 'EU', value: 4376 }, + { type: 'Lipstick', country: 'China', value: 9054 }, + { type: 'Lipstick', country: 'USA', value: 8814 }, + { type: 'Eyeshadows', country: 'Africa', value: 3308 }, + { type: 'Eyeshadows', country: 'EU', value: 4572 }, + { type: 'Eyeshadows', country: 'China', value: 12043 }, + { type: 'Eyeshadows', country: 'USA', value: 12998 }, + { type: 'Eyeliner', country: 'Africa', value: 5432 }, + { type: 'Eyeliner', country: 'EU', value: 3417 }, + { type: 'Eyeliner', country: 'China', value: 15067 }, + { type: 'Eyeliner', country: 'USA', value: 12321 }, + { type: 'Foundation', country: 'Africa', value: 13701 }, + { type: 'Foundation', country: 'EU', value: 5231 }, + { type: 'Foundation', country: 'China', value: 10119 }, + { type: 'Foundation', country: 'USA', value: 10342 }, + { type: 'Lip gloss', country: 'Africa', value: 4008 }, + { type: 'Lip gloss', country: 'EU', value: 4572 }, + { type: 'Lip gloss', country: 'China', value: 12043 }, + { type: 'Lip gloss', country: 'USA', value: 22998 }, + { type: 'Mascara', country: 'Africa', value: 18712 }, + { type: 'Mascara', country: 'EU', value: 6134 }, + { type: 'Mascara', country: 'China', value: 10419 }, + { type: 'Mascara', country: 'USA', value: 11261 } + ] + }, + title: { visible: true, text: 'Stacked area chart of cosmetic products sales' }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [{ visible: true, position: 'middle', orient: 'bottom' }], + crosshair: { xField: { visible: true } }, + tooltip: { + transitionDuration: 0, + dimension: { + title: { valueStyle: { fill: 'green' } }, + content: [ + { + key: datum => datum['country'], + value: datum => datum['value'], + valueStyle: { fill: 'red', fontWeight: 'bold' } + }, + { + key: '第二行', + value: datum => datum['value'], + keyStyle: { fill: 'blue', fontWeight: 'bold' }, + valueStyle: { fontSize: 12, lineHeight: 21 } + } + ], + updateContent: prev => { + prev[2].keyStyle = { fill: 'orange', fontWeight: 'bold' }; + prev[2].valueStyle = { fill: 'orange', fontWeight: 'bold' }; + return prev; + } + } + } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'point'); + p.y = await page.evaluate( + () => window.__visualChart.getChart().getAllSeries()[0].getRegion().getLayoutStartPoint().y + 5 + ); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + const first = await page.evaluate(() => { + const c = window.__visualChart; + const texts = c + .getStage() + .findAll(g => g.name?.includes('tooltip'), true) + .flatMap(g => g.findAll?.(g => g.type === 'text' || g.type === 'richtext', true) ?? []) + .map(g => JSON.stringify(g.attribute.textConfig ?? g.attribute.text)); + return ( + [...document.querySelectorAll('[class*="tooltip"]')] + .filter(el => el.getBoundingClientRect().width > 0 && getComputedStyle(el).visibility !== 'hidden') + .map(el => el.textContent) + .join(' ') + texts.join(' ') + ); + }); + await page.mouse.move(950, 750); + await page.waitForFunction(() => !window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.evaluate(first => (window.__tooltipText = first), first); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (!window.__tooltipText?.trim() || !window.__visualChart.getTooltipHandler()?.isTooltipShown()) + throw Error('提示未显示有效内容或未恢复'); + }); + await page.evaluate(() => { + if (!window.__tooltipText.includes('第二行')) throw Error('维度 Tooltip 额外行缺失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-disabled.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-disabled.mjs new file mode 100644 index 0000000000..a89318688d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-disabled.mjs @@ -0,0 +1,175 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0391 + * 验证目的:关闭提示框时鼠标事件仍实际发生。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '860' + }, + { + x: '1', + type: 'A', + y: '721' + }, + { + x: '2', + type: 'A', + y: '653' + }, + { + x: '3', + type: 'A', + y: '726' + }, + { + x: '4', + type: 'A', + y: '645' + }, + { + x: '5', + type: 'A', + y: '853' + }, + { + x: '6', + type: 'A', + y: '602' + }, + { + x: '7', + type: 'A', + y: '810' + }, + { + x: '8', + type: 'A', + y: '879' + }, + { + x: '9', + type: 'A', + y: '837' + }, + { + x: '0', + type: 'B', + y: '816' + }, + { + x: '1', + type: 'B', + y: '608' + }, + { + x: '2', + type: 'B', + y: '787' + }, + { + x: '3', + type: 'B', + y: '704' + }, + { + x: '4', + type: 'B', + y: '791' + }, + { + x: '5', + type: 'B', + y: '849' + }, + { + x: '6', + type: 'B', + y: '893' + }, + { + x: '7', + type: 'B', + y: '686' + }, + { + x: '8', + type: 'B', + y: '638' + }, + { + x: '9', + type: 'B', + y: '810' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6 + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + tooltip: { + visible: false + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__pointerObserved === true); + await page.evaluate(() => { + if (window.__visualChart.getTooltipHandler()?.isTooltipShown()) throw Error('禁用的tooltip仍然显示'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-hover.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-hover.mjs new file mode 100644 index 0000000000..73e2c575d2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-hover.mjs @@ -0,0 +1,47 @@ +import {} from '../../../helpers.mjs'; + +/** + * 验证目的:鼠标悬停后的 HTML tooltip(tooltip)。 + * 图表类型:bar。 + * 关键配置:tooltip.visible、tooltip.renderMode、tooltip.transitionDuration。 + * 场景条件:固定三根正值柱,定位到 B 柱内部。 + * 最终检查:鼠标悬停后验证 HTML tooltip 可见且含数值 50,再截取页面。 + * 覆盖边界:不验证其他触发模式、维度 tooltip 或自定义内容。 + */ +export default { + createSpec() { + // 单组正值柱图便于可靠定位柱体内部。 + return { + type: 'bar', + data: { + id: 'data', + values: [ + { x: 'A', y: 30 }, + { x: 'B', y: 50 }, + { x: 'C', y: 20 } + ] + }, + xField: 'x', + yField: 'y', + tooltip: { visible: true, renderMode: 'html', transitionDuration: 0 } + }; + }, + async exercise(page) { + // 使用公开坐标转换 API,验证 tooltip 内容而非只触发 mousemove。 + const point = await page.evaluate(() => window.__visualChart.convertDatumToPosition({ x: 'B', y: 25 }, {}, true)); + if (!point) throw new Error('无法定位 tooltip 目标'); + await page.mouse.move(point.x, point.y); + }, + async verify(page) { + // 目标 tooltip 必须可见且包含期望值。 + await page.waitForFunction(() => + [...document.querySelectorAll('[class*="tooltip"]')].some( + el => + el.textContent.includes('50') && + el.getBoundingClientRect().width > 0 && + getComputedStyle(el).visibility !== 'hidden' && + getComputedStyle(el).display !== 'none' + ) + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-bottom.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-bottom.mjs new file mode 100644 index 0000000000..260746d90e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-bottom.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e040f + * 验证目的:mark提示框底部定位。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 200, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + transitionDuration: 0, + mark: { + position: 'bottom' + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-inside.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-inside.mjs new file mode 100644 index 0000000000..882f9d3f37 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-inside.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0410 + * 验证目的:mark提示框图元内部定位。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + transitionDuration: 0, + mark: { + position: 'inside' + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-left.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-left.mjs new file mode 100644 index 0000000000..d07a4489f4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-left.mjs @@ -0,0 +1,85 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e040d + * 验证目的:mark提示框左侧定位。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + mark: { + position: 'left' + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-right.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-right.mjs new file mode 100644 index 0000000000..e79970bab0 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-right.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e040e + * 验证目的:mark提示框右侧定位。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + transitionDuration: 0, + mark: { + position: 'right' + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-top.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-top.mjs new file mode 100644 index 0000000000..c109d62d66 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-position-top.mjs @@ -0,0 +1,86 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e040c + * 验证目的:mark提示框顶部定位。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + transitionDuration: 0, + mark: { + position: 'top' + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-series-dimension.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-series-dimension.mjs new file mode 100644 index 0000000000..8a0fc8261e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-series-dimension.mjs @@ -0,0 +1,248 @@ +import { verifySourceSpec, verifyRendered } from '../../../helpers.mjs'; +import { interactionTarget } from '../../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7eef52a1e9eec95f9e81 + * 迁移边界:两系列源 mark/dimension 内容回调相同;当前悬停第一系列柱体验证内容,尚未覆盖两个区域所有触发组合。 + * 验证目的:来源 Tooltip 配置、实际提示内容及移出隐藏。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const femaleData = [ + { age: '0-4', type: 'Female', population: 424322, ratio: 0.06948286663944045 }, + { age: '5-9', type: 'Female', population: 410397, ratio: 0.06720264332329326 }, + { age: '10-14', type: 'Female', population: 394139, ratio: 0.06454039049213196 }, + { age: '15-19', type: 'Female', population: 320536, ratio: 0.0524878751069699 }, + { age: '20-24', type: 'Female', population: 259187, ratio: 0.04244195624001737 }, + { age: '25-29', type: 'Female', population: 218716, ratio: 0.03581481671917048 }, + { age: '30-34', type: 'Female', population: 197081, ratio: 0.03227207837483695 }, + { age: '35-39', type: 'Female', population: 183818, ratio: 0.030100257775766196 }, + { age: '40-44', type: 'Female', population: 159793, ratio: 0.026166156147727687 }, + { age: '45-49', type: 'Female', population: 137587, ratio: 0.022529916366157525 }, + { age: '50-54', type: 'Female', population: 109848, ratio: 0.017987646020261157 }, + { age: '55-59', type: 'Female', population: 86294, ratio: 0.014130670796668271 }, + { age: '60-64', type: 'Female', population: 61593, ratio: 0.010085873946962578 }, + { age: '65-69', type: 'Female', population: 41738, ratio: 0.006834611186308901 }, + { age: '70-74', type: 'Female', population: 25694, ratio: 0.004207400925320353 }, + { age: '75-79', type: 'Female', population: 14716, ratio: 0.0024097498255240255 }, + { age: '80-84', type: 'Female', population: 7482, ratio: 0.0012251799534228568 }, + { age: '85-89', type: 'Female', population: 3063, ratio: 0.000501567254388427 }, + { age: '90-94', type: 'Female', population: 798, ratio: 0.00013067276167220526 }, + { age: '95-99', type: 'Female', population: 115, ratio: 0.000018831287709653637 }, + { age: '100+', type: 'Female', population: 5, ratio: 8.187516395501582e-7 } + ]; + const maleData = [ + { age: '0-4', type: 'Male', population: 432671, ratio: 0.0708500181271613 }, + { age: '5-9', type: 'Male', population: 418331, ratio: 0.06850183842493145 }, + { age: '10-14', type: 'Male', population: 400118, ratio: 0.06551945370270604 }, + { age: '15-19', type: 'Male', population: 324150, ratio: 0.05307966879203676 }, + { age: '20-24', type: 'Male', population: 261253, ratio: 0.042780264417479494 }, + { age: '25-29', type: 'Male', population: 219584, ratio: 0.03595695200379639 }, + { age: '30-34', type: 'Male', population: 196940, ratio: 0.03224898957860163 }, + { age: '35-39', type: 'Male', population: 182855, ratio: 0.029942566209988836 }, + { age: '40-44', type: 'Male', population: 159396, ratio: 0.026101147267547403 }, + { age: '45-49', type: 'Male', population: 135222, ratio: 0.022142646840650298 }, + { age: '50-54', type: 'Male', population: 105681, ratio: 0.017305298403860053 }, + { age: '55-59', type: 'Male', population: 80496, ratio: 0.013181246395445908 }, + { age: '60-64', type: 'Male', population: 57129, ratio: 0.009354892483172197 }, + { age: '65-69', type: 'Male', population: 36260, ratio: 0.005937586890017747 }, + { age: '70-74', type: 'Male', population: 20902, ratio: 0.003422709353975481 }, + { age: '75-79', type: 'Male', population: 11231, ratio: 0.0018390799327575654 }, + { age: '80-84', type: 'Male', population: 5326, ratio: 0.0008721342464488285 }, + { age: '85-89', type: 'Male', population: 1886, ratio: 0.00030883311843831965 }, + { age: '90-94', type: 'Male', population: 441, ratio: 0.00007221389460832395 }, + { age: '95-99', type: 'Male', population: 62, ratio: 0.000010152520330421962 }, + { age: '100+', type: 'Male', population: 2, ratio: 3.275006558200633e-7 } + ]; + const spec = { + type: 'common', + title: { id: 'title', visible: true, text: '2021 年刚果人口金字塔', subtext: 'Public data source' }, + layout: { + type: 'grid', + col: 4, + row: 4, + elements: [ + { modelId: 'title', col: 0, row: 0, colSpan: 4 }, + { modelId: 'legend', col: 0, row: 3, colSpan: 4 }, + { modelId: 'leftAxesCountry', col: 0, row: 1 }, + { modelId: 'leftRegion', col: 1, row: 1 }, + { modelId: 'leftAxesValue', col: 1, row: 2 }, + { modelId: 'rightRegion', col: 2, row: 1 }, + { modelId: 'rightAxesCountry', col: 3, row: 1 }, + { modelId: 'rightAxesValue', col: 2, row: 2 } + ] + }, + region: [{ id: 'leftRegion' }, { id: 'rightRegion' }], + legends: [{ visible: true, orient: 'bottom', id: 'legend', interactive: false }], + series: [ + { + id: 'male', + regionId: 'leftRegion', + type: 'bar', + data: { id: 'maleData', values: maleData }, + direction: 'horizontal', + xField: 'ratio', + yField: 'age', + bar: { style: { fill: 'steelblue' } }, + label: { + visible: true, + position: 'left', + style: { fill: '#6F6F6F' }, + formatMethod: val => `${(val * 100).toFixed(0)}%` + }, + tooltip: { + mark: { + content: [ + { + key: datum => datum.age, + value: datum => datum.population + } + ] + }, + dimension: { + content: [ + { + key: datum => datum.age, + value: datum => datum.population + } + ] + } + } + }, + { + id: 'female', + regionId: 'rightRegion', + type: 'bar', + data: { + id: 'femaleData', + values: femaleData + }, + direction: 'horizontal', + xField: 'ratio', + yField: 'age', + bar: { + style: { + fill: '#EE7989' + } + }, + label: { + visible: true, + style: { + fill: '#6F6F6F' + }, + formatMethod: val => `${(val * 100).toFixed(0)}%` + }, + tooltip: { + mark: { + content: [ + { + key: datum => datum.age, + value: datum => datum.population + } + ] + }, + dimension: { + content: [ + { + key: datum => datum.age, + value: datum => datum.population + } + ] + } + } + } + ], + axes: [ + { + id: 'leftAxesCountry', + regionId: 'leftRegion', + seriesId: ['male'], + orient: 'left', + type: 'band', + grid: { visible: false }, + label: { + autoLimit: false + } + }, + { + id: 'rightAxesCountry', + regionId: 'rightRegion', + seriesId: ['female'], + orient: 'right', + type: 'band', + grid: { visible: false }, + label: { + autoLimit: false + } + }, + { + id: 'leftAxesValue', + regionId: 'leftRegion', + seriesId: ['male'], + orient: 'bottom', + type: 'linear', + inverse: true, + min: 0, + max: 0.1, + label: { + formatMethod: val => { + return `${(val * 100).toFixed(0)}%`; + } + } + }, + { + id: 'rightAxesValue', + regionId: 'rightRegion', + seriesId: ['female'], + orient: 'bottom', + type: 'linear', + min: 0, + max: 0.1, + label: { + formatMethod: val => { + return `${(val * 100).toFixed(0)}%`; + } + } + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'bar'); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + const first = await page.evaluate(() => { + const c = window.__visualChart; + const texts = c + .getStage() + .findAll(g => g.name?.includes('tooltip'), true) + .flatMap(g => g.findAll?.(g => g.type === 'text' || g.type === 'richtext', true) ?? []) + .map(g => JSON.stringify(g.attribute.textConfig ?? g.attribute.text)); + return ( + [...document.querySelectorAll('[class*="tooltip"]')] + .filter(el => el.getBoundingClientRect().width > 0 && getComputedStyle(el).visibility !== 'hidden') + .map(el => el.textContent) + .join(' ') + texts.join(' ') + ); + }); + await page.mouse.move(950, 750); + await page.waitForFunction(() => !window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.evaluate(first => (window.__tooltipText = first), first); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (!window.__tooltipText?.trim() || !window.__visualChart.getTooltipHandler()?.isTooltipShown()) + throw Error('提示未显示有效内容或未恢复'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-title-hidden.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-title-hidden.mjs new file mode 100644 index 0000000000..ed42889e4d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-title-hidden.mjs @@ -0,0 +1,87 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0407 + * 验证目的:隐藏mark提示框标题。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'bar', + xField: 'date', + yField: 'value', + width: 600, + height: 600, + data: [ + { + name: 'barData', + values: [ + { + date: '2019-08-29', + value: 39359021311 + }, + { + date: '2019-08-30', + value: 41363548585 + } + ] + } + ], + tooltip: { + mark: { + title: { + visible: false + } + } + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-trigger-off-none.mjs b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-trigger-off-none.mjs new file mode 100644 index 0000000000..4e25c2b725 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/components/tooltip/tooltip-trigger-off-none.mjs @@ -0,0 +1,196 @@ +import { verifySpec, verifyRendered } from '../../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e038b + * 验证目的:移出图元后保持提示框。 + * 保留条件:原始数据与配置;通用文字翻译保留字段关联和长标签场景。 + * 覆盖边界:按图元适配来源鼠标动作,检查最终提示框状态。 + */ +export default { + createSpec() { + // 返回来源的独立确定性配置。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '860' + }, + { + x: '1', + type: 'A', + y: '721' + }, + { + x: '2', + type: 'A', + y: '653' + }, + { + x: '3', + type: 'A', + y: '726' + }, + { + x: '4', + type: 'A', + y: '645' + }, + { + x: '5', + type: 'A', + y: '853' + }, + { + x: '6', + type: 'A', + y: '602' + }, + { + x: '7', + type: 'A', + y: '810' + }, + { + x: '8', + type: 'A', + y: '879' + }, + { + x: '9', + type: 'A', + y: '837' + }, + { + x: '0', + type: 'B', + y: '816' + }, + { + x: '1', + type: 'B', + y: '608' + }, + { + x: '2', + type: 'B', + y: '787' + }, + { + x: '3', + type: 'B', + y: '704' + }, + { + x: '4', + type: 'B', + y: '791' + }, + { + x: '5', + type: 'B', + y: '849' + }, + { + x: '6', + type: 'B', + y: '893' + }, + { + x: '7', + type: 'B', + y: '686' + }, + { + x: '8', + type: 'B', + y: '638' + }, + { + x: '9', + type: 'B', + y: '810' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6 + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + tooltip: { + visible: true, + triggerOff: 'none' + } + }; + }, + async exercise(page) { + // 真实鼠标操作必须产生可观察的组件状态。 + await page.evaluate(() => { + window.__visualChart.on('pointermove', () => { + window.__pointerObserved = true; + }); + }); + const p = await page.evaluate(() => { + const s = window.__visualChart.getChart().getAllSeries()[0]; + const m = s.type === 'line' ? s.getMarks().find(m => m.name === 'point') : s.getSeriesMark(); + const g = m.getGraphics()[Math.min(2, m.getGraphics().length - 1)]; + const b = g.globalAABBBounds; + window.__tooltipExpectedValue = String( + s.getViewData().latestData[Math.min(2, m.getGraphics().length - 1)][s.getSpec().yField] + ); + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.move(p.x, p.y); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown()); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 配置和绘制检查与视觉差异共同验证目标条件。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page); + await page.waitForFunction(() => window.__visualChart.getTooltipHandler()?.isTooltipShown() === true); + await page.evaluate(() => { + const spec = window.__visualChart.getSpec(); + if (spec.tooltip.renderMode === 'canvas') { + const graphics = window.__visualChart + .getStage() + .findAll(g => g.name?.includes('tooltip') && g.attribute.visible !== false, true); + if (!graphics.length) throw Error('Canvas tooltip未绘制'); + } else { + const elements = [...document.querySelectorAll('[class*="tooltip"]')]; + if ( + !elements.some( + e => + e.textContent.includes(window.__tooltipExpectedValue) && + e.getBoundingClientRect().width > 0 && + getComputedStyle(e).visibility !== 'hidden' && + getComputedStyle(e).display !== 'none' + ) + ) + throw Error('HTML tooltip没有可见内容'); + } + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/data/data-fields-domain.mjs b/packages/vchart/__tests__/visual/cases/data/data-fields-domain.mjs new file mode 100644 index 0000000000..97bb35ddac --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/data/data-fields-domain.mjs @@ -0,0 +1,309 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 649d7efc52a1e9eec95f9ea8 + * 验证目的:数据字段定义域过滤异常数据并保留域内点。 + * 保留条件:xField, yField, axes, title;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'scatter', + data: [ + { + id: 'data', + values: [ + { + height: 150, + weight: 45.5 + }, + { + height: 152, + weight: 52.5 + }, + { + height: 154.3, + weight: 53.5 + }, + { + height: 161.6, + weight: 53.5 + }, + { + height: 156, + weight: 55.5 + }, + { + height: 158, + weight: 57.5 + }, + { + height: 160, + weight: 52.5 + }, + { + height: 161, + weight: 58.5 + }, + { + height: 160.4, + weight: 56.5 + }, + { + height: 160.6, + weight: 64.5 + }, + { + height: 162, + weight: 54.5 + }, + { + height: 162, + weight: 54.5 + }, + { + height: 162, + weight: 56.5 + }, + { + height: 162, + weight: 58.5 + }, + { + height: 162, + weight: 64.5 + }, + { + height: 161, + weight: 56.5 + }, + { + height: 164, + weight: 60.5 + }, + { + height: 166, + weight: 64.5 + }, + { + height: 168, + weight: 67.5 + }, + { + height: 170, + weight: 63.5 + }, + { + height: 172, + weight: 64.5 + }, + { + height: 174, + weight: 65.5 + }, + { + height: 176, + weight: 65.5 + }, + { + height: 170, + weight: 70 + }, + { + height: 170, + weight: 70 + }, + { + height: 170, + weight: 70 + }, + { + height: 170, + weight: 70 + }, + { + height: 170, + weight: 70 + }, + { + height: 170, + weight: 70 + }, + { + height: 160, + weight: 59 + }, + { + height: 173, + weight: 75 + }, + { + height: 165, + weight: 68 + }, + { + height: 167, + weight: 69 + }, + { + height: 174, + weight: 75 + }, + { + height: 173.4, + weight: 75 + }, + { + height: 173, + weight: 72 + }, + { + height: 173, + weight: 76 + }, + { + height: 171, + weight: 71 + }, + { + height: 172, + weight: 70 + }, + { + height: 172, + weight: 70 + }, + { + height: 172, + weight: 70 + }, + { + height: 172, + weight: 70 + }, + { + height: 175, + weight: 76 + }, + { + height: 174, + weight: 79 + }, + { + height: 172, + weight: 81 + }, + { + height: 172, + weight: 70 + }, + { + height: 170, + weight: 73 + }, + { + height: 185, + weight: 80 + }, + { + height: 1.89, + weight: 86 + }, + { + height: 1.7, + weight: 86 + }, + { + height: 1.8, + weight: 80 + }, + { + height: 1.7, + weight: 75 + }, + { + height: 1.6, + weight: 62 + }, + { + height: 1.65, + weight: 70 + }, + { + height: 1.58, + weight: 66 + }, + { + height: 1.65, + weight: 67200 + }, + { + height: 1.55, + weight: 50000 + }, + { + height: 160, + weight: 56000 + }, + { + height: 152, + weight: 46000 + } + ], + fields: { + height: { + domain: [30, 300] + }, + weight: { + domain: [10, 200] + } + } + } + ], + xField: 'height', + yField: 'weight', + axes: [ + { + orient: 'left', + type: 'linear', + zero: false, + title: { + visible: true, + text: 'Unit: kg' + } + }, + { + orient: 'bottom', + type: 'linear', + zero: false, + title: { + visible: true, + text: 'Unit: cm' + } + } + ], + title: { + visible: true, + text: 'Height and weight sample with invalid entries' + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + const expected = this.createSpec().data[0].values.filter( + d => d.height >= 30 && d.height <= 300 && d.weight >= 10 && d.weight <= 200 + ); + await page.evaluate(expected => { + const series = window.__visualChart.getChart().getAllSeries()[0]; + const values = series.getViewData().latestData.map(d => ({ height: d.height, weight: d.weight })); + if (JSON.stringify(values) !== JSON.stringify(expected)) + throw new Error('字段定义域未按来源过滤异常数据或丢失重复点'); + if (series.getSeriesMark().getGraphics().length !== expected.length) throw new Error('域内数据未完整绘制'); + }, expected); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/data/scale-domain-expand.mjs b/packages/vchart/__tests__/visual/cases/data/scale-domain-expand.mjs new file mode 100644 index 0000000000..9d64bddaa8 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/data/scale-domain-expand.mjs @@ -0,0 +1,267 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0449 + * 验证目的:多系列比例尺定义域扩展。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + data: [ + { + id: 'data0', + values: [ + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + }, + { + id: 'data1', + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + } + ] + } + ], + series: [ + { + type: 'scatter', + data: { + id: 'data0' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fill: { + scale: 'color', + field: 'y' + }, + size: { + field: 'size', + scale: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + }, + { + type: 'scatter', + data: { + id: 'data1' + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + point: { + style: { + fill: { + scale: 'color', + field: 'y', + changeDomain: 'expand' + }, + size: { + scale: 'size', + field: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + } + ], + scales: [ + { + id: 'size', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['size'] + } + ], + range: [10, 25] + }, + { + id: 'color', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['y'] + } + ], + range: ['red', 'blue'] + }, + { + id: 'shape', + type: 'ordinal', + domain: [ + { + dataId: 'data0', + fields: ['area'] + }, + { + dataId: 'data1', + fields: ['area'] + } + ], + range: ['star', 'triangleLeft', 'diamond'] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + }, + type: 'linear' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/data/scale-domain-replace.mjs b/packages/vchart/__tests__/visual/cases/data/scale-domain-replace.mjs new file mode 100644 index 0000000000..a8a1a5504c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/data/scale-domain-replace.mjs @@ -0,0 +1,272 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e044a + * 验证目的:公共比例尺替换定义域后作用于双散点系列。 + * 保留条件:series, scales, axes;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主,翻译通用类目/说明文字(关联引用同步替换)。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'common', + data: [ + { + id: 'data0', + values: [ + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + }, + { + id: 'data1', + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + } + ] + } + ], + series: [ + { + type: 'scatter', + data: { + id: 'data0' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fill: { + scale: 'color', + field: 'y' + }, + size: { + field: 'size', + scale: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + }, + { + type: 'scatter', + data: { + id: 'data1' + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + point: { + style: { + fill: { + scale: 'color', + field: 'y', + changeDomain: 'replace' + }, + size: { + scale: 'size', + field: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + } + ], + scales: [ + { + id: 'size', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['size'] + }, + { + dataId: 'data1', + fields: ['size'] + } + ], + range: [10, 25] + }, + { + id: 'color', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['y'] + } + ], + range: ['red', 'blue'] + }, + { + id: 'shape', + type: 'ordinal', + domain: [ + { + dataId: 'data0', + fields: ['area'] + }, + { + dataId: 'data1', + fields: ['area'] + } + ], + range: ['star', 'triangleLeft', 'diamond'] + } + ], + axes: [ + { + orient: 'left', + range: { + min: 0 + }, + type: 'linear' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/data/scale-specified-shape.mjs b/packages/vchart/__tests__/visual/cases/data/scale-specified-shape.mjs new file mode 100644 index 0000000000..809d33fd17 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/data/scale-specified-shape.mjs @@ -0,0 +1,274 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; +/** + * BugServer case IDs: 64b8d52edc862dee5aa01400 + * 验证目的:公共比例尺指定值形状映射。 + * 保留条件:原始数据、顺序、字段关联和配置组合;静态 const 引用按值展开。 + * 迁移说明:仅适配本地宿主及通用文字;不改变数值或数组顺序。 + * 覆盖边界:验证当前静态状态,不计 hover/select 等交互覆盖。 + */ +export default { + createSpec() { + // 每次生成独立的源配置,避免跨用例共享可变对象。 + return { + type: 'common', + data: [ + { + id: 'data0', + values: [ + { + x: 1270911, + size: 219815, + y: 5590, + type: 'Office supplies', + area: 'Central South' + }, + { + x: 919743, + size: 148800, + y: 1199, + type: 'Furniture', + area: 'North' + }, + { + x: 1676224, + size: 163453, + y: 2517, + type: 'Furniture', + area: 'East' + }, + { + x: 824673, + size: 86067, + y: 3622, + type: 'Office supplies', + area: 'Northeast' + }, + { + x: 745813, + size: 137265, + y: 3020, + type: 'Office supplies', + area: 'North' + }, + { + x: 267870, + size: 49633, + y: 970, + type: 'Office supplies', + area: 'Northwest' + }, + { + x: 1408628, + size: 215585, + y: 6341, + type: 'Office supplies', + area: 'East' + }, + { + x: 501533, + size: 29303, + y: 814, + type: 'Furniture', + area: 'Southwest' + }, + { + x: 920698, + size: 72692, + y: 1470, + type: 'Furniture', + area: 'Northeast' + }, + { + x: 316212, + size: 24903, + y: 468, + type: 'Furniture', + area: 'Northwest' + }, + { + x: 1399928, + size: 199582, + y: 2023, + type: 'Furniture', + area: 'Central South' + }, + { + x: 347692, + size: 49272, + y: 1858, + type: 'Office supplies', + area: 'Southwest' + } + ] + }, + { + id: 'data1', + values: [ + { + x: 936196, + size: 83431, + y: 1371, + type: 'Technology', + area: 'Northeast' + }, + { + x: 453898, + size: 19061, + y: 727, + type: 'Technology', + area: 'Southwest' + }, + { + x: 1466575, + size: 251487, + y: 2087, + type: 'Technology', + area: 'Central South' + }, + { + x: 230956, + size: 24016, + y: 347, + type: 'Technology', + area: 'Northwest' + }, + { + x: 1599653, + size: 228179, + y: 2183, + type: 'Technology', + area: 'East' + }, + { + x: 781743, + size: 144986, + y: 927, + type: 'Technology', + area: 'North' + } + ] + } + ], + series: [ + { + type: 'scatter', + data: { + id: 'data0' + }, + xField: 'x', + yField: 'y', + seriesField: 'type', + point: { + style: { + fill: { + scale: 'color', + field: 'y' + }, + size: { + field: 'size', + scale: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + }, + { + type: 'scatter', + data: { + id: 'data1' + }, + seriesField: 'type', + xField: 'x', + yField: 'y', + point: { + style: { + fill: { + scale: 'color', + field: 'y' + }, + size: { + scale: 'size', + field: 'size' + }, + shape: { + scale: 'shape', + field: 'area' + } + } + } + } + ], + scales: [ + { + id: 'size', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['size'] + }, + { + dataId: 'data1', + fields: ['size'] + } + ], + range: [10, 25] + }, + { + id: 'color', + type: 'linear', + domain: [ + { + dataId: 'data0', + fields: ['y'] + }, + { + dataId: 'data1', + fields: ['y'] + } + ], + range: ['red', 'blue'] + }, + { + id: 'shape', + type: 'ordinal', + domain: [ + { + dataId: 'data0', + fields: ['area'] + }, + { + dataId: 'data1', + fields: ['area'] + } + ], + range: ['star', 'triangleLeft', 'diamond'], + specified: { + 'Central South': 'circle' + } + } + ], + axes: [ + { + orient: 'left', + type: 'linear' + }, + { + orient: 'bottom', + label: { + visible: true + }, + type: 'band' + } + ] + }; + }, + async verify(page) { + // 严格核对输入条件并检查实际图元;布局和样式由截图及受控变异验证。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/index.mjs b/packages/vchart/__tests__/visual/cases/index.mjs new file mode 100644 index 0000000000..5c76200eb4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/index.mjs @@ -0,0 +1,1306 @@ +/** + * @typedef {{id: string, purpose: string, file: string, sourceExample?: string}} CaseMetadata + * @typedef {{createSpec: () => object, exercise?: (page: import('@playwright/test').Page) => Promise, verify: (page: import('@playwright/test').Page) => Promise}} VisualCase + */ +/** 本地用例元数据的唯一清单;模块必须保持浏览器和 Node 均可导入。 */ +export const cases = [ + { + id: 'bar-stack', + purpose: '正负值、堆叠与零基准线(bar)', + file: './charts/bar/bar-stack.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/bar.ts' + }, + { + id: 'line-gap', + purpose: '缺失值与折线连接(data-zoom-brush-line)', + file: './charts/line/line-gap.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/data-zoom-brush-line.ts' + }, + { + id: 'scatter-symbol', + purpose: '散点位置、大小及符号(scatter)', + file: './charts/scatter/scatter-symbol.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/scatter.ts' + }, + { + id: 'pie-label', + purpose: '外侧标签与引导线(pie-label)', + file: './components/label/pie-label.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/pie-label.ts' + }, + { + id: 'axis-label', + purpose: '长文本、旋转和轴布局(axis-label-layout)', + file: './components/axis/axis-label.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/axis-label-layout.ts' + }, + { + id: 'waterfall', + purpose: '累计、总计与连接线(waterfall)', + file: './charts/waterfall/waterfall.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/waterfall.ts' + }, + { + id: 'legend-filter', + purpose: '图例点击筛选(multiple-legend-layout)', + file: './components/legend/legend-filter.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/multiple-legend-layout.ts' + }, + { + id: 'tooltip-hover', + purpose: '鼠标悬停后的 HTML tooltip(tooltip)', + file: './components/tooltip/tooltip-hover.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/tooltip.ts' + }, + { + id: 'datazoom-drag', + purpose: '拖动后的可视范围(datazoom)', + file: './components/datazoom/datazoom-drag.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/datazoom.ts' + }, + { + id: 'update-resize', + purpose: '数据更新与尺寸调整(event-update-spec)', + file: './api/update-resize.mjs', + sourceExample: 'packages/vchart/__tests__/runtime/browser/test-page/event-update-spec.ts' + }, + { + id: 'bar-area-smooth', + purpose: '同一区域的柱图与平滑轮廓面积图叠加', + file: './charts/combination/bar-area-smooth.mjs' + }, + { + id: 'multi-bar-inside', + purpose: '两份数据中的四个分组使用两个柱系列并显示内部标签', + file: './components/label/multi-bar-inside.mjs' + }, + { + id: 'mark-line-sum', + purpose: '求和标注线扩展数据范围', + file: './components/marker/mark-line-sum.mjs' + }, + { + id: 'pie-negative', + purpose: '混合正负值饼图同时开启全零显示和负值支持', + file: './charts/pie/pie-negative.mjs' + }, + { + id: 'bar-stack-inverse-value', + purpose: '堆叠柱图反转数值轴并显示总计标签', + file: './charts/bar/bar-stack-inverse-value.mjs' + }, + { + id: 'line-band-invalid-zero', + purpose: '双离散轴折线保留 invalidType=zero 配置', + file: './charts/line/line-band-invalid-zero.mjs' + }, + { + id: 'line-stack-dashed-legend', + purpose: '堆叠折线与底部虚线图例共存', + file: './charts/line/line-stack-dashed-legend.mjs' + }, + { + id: 'update-state-scatter-area', + purpose: '散点与面积图分别按数据过滤更新状态', + file: './api/update-state-scatter-area.mjs' + }, + { + id: 'pie-body', + purpose: '四项原始数据映射饼图扇区', + file: './charts/pie/pie-body.mjs' + }, + { + id: 'range-column-horizontal', + purpose: '横向区间柱与端点标签', + file: './charts/range-column/range-column-horizontal.mjs' + }, + { + id: 'box-plot-vertical', + purpose: '纵向箱线图五数概括与线形须线', + file: './charts/box-plot/box-plot-vertical.mjs' + }, + { + id: 'box-plot-grouped', + purpose: '分组箱线图与图例标题及分层轴间距', + file: './charts/box-plot/box-plot-grouped.mjs' + }, + { + id: 'progress-linear-gradient', + purpose: '渐变线性进度图与显式双轴', + file: './charts/progress/progress-linear-gradient.mjs' + }, + { + id: 'progress-linear-padding', + purpose: '进度主体上下留白', + file: './charts/progress/progress-linear-padding.mjs' + }, + { + id: 'progress-linear-clamp', + purpose: '超出数值轴上限的进度截断', + file: './charts/progress/progress-linear-clamp.mjs' + }, + { + id: 'progress-circular-gradient', + purpose: '圆形进度渐变及内外留白', + file: './charts/progress/progress-circular-gradient.mjs' + }, + { + id: 'grid-multiple-axes', + purpose: '三区域网格布局中多折线与独立数值轴', + file: './layout/grid-multiple-axes.mjs' + }, + { + id: 'axis-auto-hide', + purpose: '窄图表中三个长时间标签自动隐藏', + file: './components/axis/axis-auto-hide.mjs' + }, + { + id: 'label-stack-zero', + purpose: '零值堆叠柱标签与边界移动避让', + file: './components/label/label-stack-zero.mjs' + }, + { + id: 'grid-line-label-layout', + purpose: '三区域折线标签与独立轴布局', + file: './components/label/grid-line-label-layout.mjs' + }, + { + id: 'mark-line-value', + purpose: '折线点标签与水平参考线共存', + file: './components/marker/mark-line-value.mjs' + }, + { + id: 'mark-line-auto-range', + purpose: '横纵坐标的四条标注线自动扩轴', + file: './components/marker/mark-line-auto-range.mjs' + }, + { + id: 'mark-area-scatter-label', + purpose: '隐藏坐标轴时散点标签与三块坐标标注共存', + file: './components/marker/mark-area-scatter-label.mjs' + }, + { + id: 'legend-multiple', + purpose: '五个图例在四个方向同时布局', + file: './components/legend/legend-multiple.mjs' + }, + { + id: 'bar-title', + purpose: '基础柱图显示主标题', + file: './components/title/bar-title.mjs' + }, + { + id: 'area-horizontal', + purpose: '横向面积图的数据映射与坐标布局', + file: './charts/area/area-horizontal.mjs' + }, + { + id: 'area-negative', + purpose: '面积图正负值与零基线', + file: './charts/area/area-negative.mjs' + }, + { + id: 'area-missing', + purpose: '面积图缺失值及源无效数据策略', + file: './charts/area/area-missing.mjs' + }, + { + id: 'area-stacked', + purpose: '多组面积图堆叠与图例', + file: './charts/area/area-stacked.mjs' + }, + { + id: 'area-unstacked', + purpose: '多组面积图关闭堆叠后的相交布局', + file: './charts/area/area-unstacked.mjs' + }, + { + id: 'line-step', + purpose: '阶梯折线的曲线类型与点布局', + file: './charts/line/line-step.mjs' + }, + { + id: 'line-monotone', + purpose: '单调平滑折线与点布局', + file: './charts/line/line-monotone.mjs' + }, + { + id: 'bar-group', + purpose: '分组柱图的双分类字段与系列', + file: './charts/bar/bar-group.mjs' + }, + { + id: 'bar-three-level-gap', + purpose: '三层分组柱图的组内间距', + file: './charts/bar/bar-three-level-gap.mjs' + }, + { + id: 'bar-width-limit', + purpose: '柱宽与最大柱宽共同配置', + file: './charts/bar/bar-width-limit.mjs' + }, + { + id: 'histogram-min-height', + purpose: '直方图区间边界与最小柱高', + file: './charts/histogram/histogram-min-height.mjs' + }, + { + id: 'funnel-directions', + purpose: '四区域漏斗的方向、对齐与外侧标签', + file: './charts/funnel/funnel-directions.mjs' + }, + { + id: 'funnel-transform', + purpose: '转化漏斗的转化标签与外侧标签', + file: './charts/funnel/funnel-transform.mjs' + }, + { + id: 'gauge-gradient', + purpose: '仪表盘渐变圆弧和指针组件', + file: './charts/gauge/gauge-gradient.mjs' + }, + { + id: 'gauge-tick-segment', + purpose: '仪表盘刻度分段和指针', + file: './charts/gauge/gauge-tick-segment.mjs' + }, + { + id: 'rose-stack', + purpose: '玫瑰图分组数据的堆叠', + file: './charts/rose/rose-stack.mjs' + }, + { + id: 'rose-group', + purpose: '玫瑰图分组与极坐标轴', + file: './charts/rose/rose-group.mjs' + }, + { + id: 'sankey-horizontal', + purpose: '横向桑基图节点和连接布局', + file: './charts/sankey/sankey-horizontal.mjs' + }, + { + id: 'sankey-vertical', + purpose: '纵向桑基图节点和连接布局', + file: './charts/sankey/sankey-vertical.mjs' + }, + { + id: 'axis-log', + purpose: '对数坐标轴的刻度与折线位置', + file: './components/axis/axis-log.mjs' + }, + { + id: 'axis-zero-align', + purpose: '双数值轴在零点两侧对齐', + file: './components/axis/axis-zero-align.mjs' + }, + { + id: 'axis-tick-align', + purpose: '双数值轴的刻度对齐', + file: './components/axis/axis-tick-align.mjs' + }, + { + id: 'axis-polar-multiple-labels', + purpose: '极坐标多层标签布局', + file: './components/axis/axis-polar-multiple-labels.mjs' + }, + { + id: 'axis-multilevel-wrap', + purpose: '多层分类轴标签自动换行', + file: './components/axis/axis-multilevel-wrap.mjs' + }, + { + id: 'axis-break', + purpose: '数值轴断轴及柱图分段', + file: './components/axis/axis-break.mjs' + }, + { + id: 'axis-unit-position', + purpose: '轴单位在指定位置显示', + file: './components/axis/axis-unit-position.mjs' + }, + { + id: 'mark-area-multiple', + purpose: '多块标注区域与折线坐标映射', + file: './components/marker/mark-area-multiple.mjs' + }, + { + id: 'mark-line-coordinates', + purpose: '坐标点定位标注线', + file: './components/marker/mark-line-coordinates.mjs' + }, + { + id: 'scale-domain-replace', + purpose: '公共比例尺替换定义域后作用于双散点系列', + file: './data/scale-domain-replace.mjs' + }, + { + id: 'data-fields-domain', + purpose: '数据字段定义域与散点编码', + file: './data/data-fields-domain.mjs' + }, + { + id: 'combination-line-pie', + purpose: '折线和饼图在独立区域共存', + file: './charts/combination/combination-line-pie.mjs' + }, + { + id: 'pie-nested', + purpose: '多系列嵌套环形饼图', + file: './charts/pie/pie-nested.mjs' + }, + { + id: 'pie-radius-scale', + purpose: '饼图半径字段编码', + file: './charts/pie/pie-radius-scale.mjs' + }, + { + id: 'theme-stack', + purpose: '堆叠面积图的专用主题样式', + file: './theme/theme-stack.mjs' + }, + { + id: 'legend-symbol-hidden', + purpose: '图例项隐藏符号后的文本布局', + file: './components/legend/legend-symbol-hidden.mjs' + }, + { + id: 'scrollbar-axis-range', + purpose: '隐藏滚动条的初始范围及分类轴显示', + file: './components/scrollbar/scrollbar-axis-range.mjs' + }, + { + id: 'datazoom-preview', + purpose: 'dataZoom 预览图的初始范围和布局', + file: './components/datazoom/datazoom-preview.mjs' + }, + { + id: 'waterfall-leader-line', + purpose: '反向类目轴瀑布图的连接线与变化值堆叠标签', + file: './charts/waterfall/waterfall-leader-line.mjs' + }, + { + id: 'label-smart-invert', + purpose: '柱图外侧标签的智能反色配置', + file: './components/label/label-smart-invert.mjs' + }, + { + id: 'radar-series', + purpose: '雷达图多系列与角度和半径轴', + file: './charts/radar/radar-series.mjs' + }, + { + id: 'radar-negative', + purpose: '雷达图负值与面积区域', + file: './charts/radar/radar-negative.mjs' + }, + { + id: 'radar-stacked', + purpose: '雷达面积堆叠与圆形径向网格', + file: './charts/radar/radar-stacked.mjs' + }, + { + id: 'range-area-horizontal', + purpose: '横向区间面积图的上下界', + file: './charts/range-area/range-area-horizontal.mjs' + }, + { + id: 'range-area-missing-bound', + purpose: '区间面积上界缺失并叠加两条折线', + file: './charts/range-area/range-area-missing-bound.mjs' + }, + { + id: 'pie-empty-placeholder', + purpose: '空数据时显示自定义饼图占位环', + file: './charts/pie/pie-empty-placeholder.mjs' + }, + { + id: 'pie-zero-placeholder', + purpose: '全零数据时显示饼图占位环', + file: './charts/pie/pie-zero-placeholder.mjs' + }, + { + id: 'pie-show-all-zero', + purpose: 'showAllZero 下全零数据的扇区及外侧标签', + file: './charts/pie/pie-show-all-zero.mjs' + }, + { + id: 'word-cloud-enlarge', + purpose: '词云的确定性布局与放大配置', + file: './charts/word-cloud/word-cloud-enlarge.mjs' + }, + { + id: 'sunburst-gap', + purpose: '旭日图分层间隙和径向标签', + file: './charts/sunburst/sunburst-gap.mjs' + }, + { + id: 'circle-packing-padding', + purpose: '圆打包分层留白和按深度显示标签', + file: './charts/circle-packing/circle-packing-padding.mjs' + }, + { + id: 'treemap-hierarchy', + purpose: '矩形树图的层次数据与标签', + file: './charts/treemap/treemap-hierarchy.mjs' + }, + { + id: 'heatmap-correlation', + purpose: '相关矩阵热力图与固定区域及旋转标签', + file: './charts/heatmap/heatmap-correlation.mjs' + }, + { + id: 'bar-percent', + purpose: '百分比堆叠柱图与百分数刻度格式', + file: './charts/bar/bar-percent.mjs' + }, + { + id: 'axis-symlog', + purpose: '相同正负数据在线性轴和对称对数轴的布局', + file: './components/axis/axis-symlog.mjs' + }, + { + id: 'mark-point-symbol', + purpose: '标注点文字、端点配置与动态轴标签', + file: './components/marker/mark-point-symbol.mjs' + }, + { + id: 'crosshair-polar-default', + purpose: '极坐标默认选中的径向和角度 crosshair', + file: './components/crosshair/crosshair-polar-default.mjs' + }, + { + id: 'update-indicator-visible', + purpose: 'updateSpecSync 后显示仪表指标文字', + file: './api/update-indicator-visible.mjs' + }, + { + id: 'update-pie-empty', + purpose: '更新饼图数据为 null 和零后切换占位图', + file: './api/update-pie-empty.mjs' + }, + { + id: 'brush-select', + purpose: '拖拽矩形刷选后区分命中和未命中的散点', + file: './components/brush/brush-select.mjs' + }, + { + id: 'custom-mark-click', + purpose: '按 markName 绑定的自定义图元点击更新', + file: './interaction/custom-mark-click.mjs' + }, + { + id: 'legend-continuous-filter', + purpose: '拖动连续颜色图例后筛选矩形树图数据', + file: './components/legend/legend-continuous-filter.mjs' + }, + { + id: 'line-missing-link', + purpose: '连接缺失值的折线与多系列数据', + file: './charts/line/line-missing-link.mjs' + }, + { + id: 'area-invalid-zero', + purpose: '缺失值按零处理的堆叠面积与标签', + file: './charts/area/area-invalid-zero.mjs' + }, + { + id: 'axis-time-brush', + purpose: '时间轴和 dataZoom 及刷选配置共存', + file: './components/axis/axis-time-brush.mjs' + }, + { + id: 'correlation-tooltip', + purpose: '关联图布局与悬停后的 tooltip', + file: './charts/correlation/correlation-tooltip.mjs' + }, + { + id: 'marker-end-symbols', + purpose: '标注线端点符号与显隐组合', + file: './components/marker/marker-end-symbols.mjs' + }, + { + id: 'marker-label-background', + purpose: '标注线标签背景与偏移布局', + file: './components/marker/marker-label-background.mjs' + }, + { + id: 'marker-statistics', + purpose: '最小最大和平均值标注线', + file: './components/marker/marker-statistics.mjs' + }, + { + id: 'marker-absolute-position', + purpose: '绝对像素坐标标注线', + file: './components/marker/marker-absolute-position.mjs' + }, + { + id: 'marker-region-position', + purpose: '区域相对百分比坐标标注线', + file: './components/marker/marker-region-position.mjs' + }, + { + id: 'marker-monotone-line', + purpose: '标注线单调曲线路径', + file: './components/marker/marker-monotone-line.mjs' + }, + { + id: 'marker-polar-coordinates', + purpose: '极坐标角度半径标注线和标注点', + file: './components/marker/marker-polar-coordinates.mjs' + }, + { + id: 'scatter-ordinal-shape', + purpose: '散点形状与线性大小编码', + file: './charts/scatter/scatter-ordinal-shape.mjs' + }, + { + id: 'scatter-ordinal-size', + purpose: '散点离散大小编码', + file: './charts/scatter/scatter-ordinal-size.mjs' + }, + { + id: 'scale-domain-expand', + purpose: '多系列比例尺定义域扩展', + file: './data/scale-domain-expand.mjs' + }, + { + id: 'scale-specified-shape', + purpose: '公共比例尺指定值形状映射', + file: './data/scale-specified-shape.mjs' + }, + { + id: 'area-stream-center', + purpose: '流图堆叠偏移和面积布局', + file: './charts/area/area-stream-center.mjs' + }, + { + id: 'axis-end-symbol', + purpose: '坐标轴端点符号', + file: './components/axis/axis-end-symbol.mjs' + }, + { + id: 'axis-explicit-domain', + purpose: '显式轴定义域与多系列', + file: './components/axis/axis-explicit-domain.mjs' + }, + { + id: 'axis-auto-rotate', + purpose: '分类轴标签自动旋转', + file: './components/axis/axis-auto-rotate.mjs' + }, + { + id: 'axis-vertical-text', + purpose: '坐标轴标签竖排文字', + file: './components/axis/axis-vertical-text.mjs' + }, + { + id: 'axis-visible-layers', + purpose: '分类轴多层标签显示', + file: './components/axis/axis-visible-layers.mjs' + }, + { + id: 'axis-trim-padding', + purpose: '面积图分类轴两端留白裁剪', + file: './components/axis/axis-trim-padding.mjs' + }, + { + id: 'axis-unit-offset', + purpose: '自动缩进与单位文本偏移', + file: './components/axis/axis-unit-offset.mjs' + }, + { + id: 'bar-stack-corner', + purpose: '堆叠柱整体圆角和自动带宽', + file: './charts/bar/bar-stack-corner.mjs' + }, + { + id: 'bar-min-width', + purpose: '最小柱宽与自动带宽', + file: './charts/bar/bar-min-width.mjs' + }, + { + id: 'bar-group-gap', + purpose: '分组柱的组内间距', + file: './charts/bar/bar-group-gap.mjs' + }, + { + id: 'bar-gradient-corner', + purpose: '渐变柱形与圆角', + file: './charts/bar/bar-gradient-corner.mjs' + }, + { + id: 'bar-gradient-label', + purpose: '透明渐变柱与标签布局', + file: './components/label/bar-gradient-label.mjs' + }, + { + id: 'box-plot-bar-shaft', + purpose: '箱线图柱形须线和宽度', + file: './charts/box-plot/box-plot-bar-shaft.mjs' + }, + { + id: 'area-series-mark', + purpose: '面积图指定主图元', + file: './charts/area/area-series-mark.mjs' + }, + { + id: 'area-step-curve', + purpose: '面积图阶梯曲线', + file: './charts/area/area-step-curve.mjs' + }, + { + id: 'area-point-outer-border', + purpose: '面积图数据点外描边', + file: './charts/area/area-point-outer-border.mjs' + }, + { + id: 'sankey-node-align', + purpose: '桑基节点左对齐', + file: './charts/sankey/sankey-node-align.mjs' + }, + { + id: 'sankey-cross-align-start', + purpose: '桑基节点交叉方向起点对齐', + file: './charts/sankey/sankey-cross-align-start.mjs' + }, + { + id: 'sankey-cross-align-end', + purpose: '桑基节点交叉方向末端对齐', + file: './charts/sankey/sankey-cross-align-end.mjs' + }, + { + id: 'sankey-cross-align-middle', + purpose: '桑基节点交叉方向中间对齐', + file: './charts/sankey/sankey-cross-align-middle.mjs' + }, + { + id: 'progress-tick-mask', + purpose: '环形进度刻度遮罩、强制对齐及悬停指标', + file: './charts/progress/progress-tick-mask.mjs' + }, + { + id: 'progress-series-track', + purpose: '环形进度系列级轨道配置', + file: './charts/progress/progress-series-track.mjs' + }, + { + id: 'scatter-radial-gradient', + purpose: '散点径向渐变填充', + file: './charts/scatter/scatter-radial-gradient.mjs' + }, + { + id: 'crosshair-dual-axis-default', + purpose: '双轴绑定的默认选中十字线', + file: './components/crosshair/crosshair-dual-axis-default.mjs' + }, + { + id: 'datazoom-vertical-initial-range', + purpose: '纵向 dataZoom 初始范围与轴窗口', + file: './components/datazoom/datazoom-vertical-initial-range.mjs' + }, + { + id: 'scrollbar-range-mode', + purpose: '滚动条值和比例混合范围', + file: './components/scrollbar/scrollbar-range-mode.mjs' + }, + { + id: 'line-end-label', + purpose: '折线末端标签', + file: './components/label/line-end-label.mjs' + }, + { + id: 'waterfall-horizontal-total', + purpose: '横向瀑布图字段总计与累计连接', + file: './charts/waterfall/waterfall-horizontal-total.mjs' + }, + { + id: 'waterfall-absolute-label', + purpose: '瀑布图绝对值堆叠标签与字段总计', + file: './charts/waterfall/waterfall-absolute-label.mjs' + }, + { + id: 'waterfall-total-tag', + purpose: '瀑布图仅通过标记字段识别总计', + file: './charts/waterfall/waterfall-total-tag.mjs' + }, + { + id: 'richtext-axis-marker', + purpose: '轴标题标签及标注区域富文本', + file: './components/richtext/richtext-axis-marker.mjs' + }, + { + id: 'richtext-funnel-label', + purpose: '漏斗内外标签富文本和装饰', + file: './components/richtext/richtext-funnel-label.mjs' + }, + { + id: 'richtext-pie-title', + purpose: '饼图富文本标题副标题及标签', + file: './components/richtext/richtext-pie-title.mjs' + }, + { + id: 'richtext-range-label', + purpose: '区间柱富文本标签', + file: './components/richtext/richtext-range-label.mjs' + }, + { + id: 'richtext-line-end-label', + purpose: '折线末端富文本标签', + file: './components/richtext/richtext-line-end-label.mjs' + }, + { + id: 'richtext-marker-label', + purpose: '散点标注线富文本标签', + file: './components/richtext/richtext-marker-label.mjs' + }, + { + id: 'richtext-extension-mark', + purpose: '组合图自定义富文本图元', + file: './components/richtext/richtext-extension-mark.mjs' + }, + { + id: 'treemap-nonleaf-label', + purpose: '矩形树图父节点显示与标签格式化', + file: './charts/treemap/treemap-nonleaf-label.mjs' + }, + { + id: 'axis-polar-title-background', + purpose: '极坐标轴标题背景与内部刻度', + file: './components/axis/axis-polar-title-background.mjs' + }, + { + id: 'indicator-autofit', + purpose: '环图指标文字自动适应', + file: './components/indicator/indicator-autofit.mjs' + }, + { + id: 'marker-format-statistics', + purpose: '统计标注区域与标注线标签回调', + file: './components/marker/marker-format-statistics.mjs' + }, + { + id: 'marker-label-confine', + purpose: '标注线标签边界约束与端点符号', + file: './components/marker/marker-label-confine.mjs' + }, + { + id: 'marker-multi-segment', + purpose: '多段标注线连接方向及扩展距离', + file: './components/marker/marker-multi-segment.mjs' + }, + { + id: 'marker-expand-callback', + purpose: '多段标注线扩展距离回调', + file: './components/marker/marker-expand-callback.mjs' + }, + { + id: 'marker-background-shape', + purpose: '标注线标签自定义背景路径', + file: './components/marker/marker-background-shape.mjs' + }, + { + id: 'waterfall-total-callback', + purpose: '瀑布图总计计算回调', + file: './charts/waterfall/waterfall-total-callback.mjs' + }, + { + id: 'waterfall-summary-total', + purpose: '瀑布图自动汇总与标签格式化', + file: './charts/waterfall/waterfall-summary-total.mjs' + }, + { + id: 'axis-label-callback', + purpose: '坐标轴标签格式化回调', + file: './components/axis/axis-label-callback.mjs' + }, + { + id: 'scatter-size-callback', + purpose: '反向坐标轴与散点大小回调', + file: './charts/scatter/scatter-size-callback.mjs' + }, + { + id: 'scatter-shape-callback', + purpose: '散点形状大小回调编码', + file: './charts/scatter/scatter-shape-callback.mjs' + }, + { + id: 'area-selected-state', + purpose: '面积图数据点点击选中', + file: './interaction/area-selected-state.mjs' + }, + { + id: 'combination-hover-reverse', + purpose: '混合系列悬停与反向弱化状态', + file: './interaction/combination-hover-reverse.mjs' + }, + { + id: 'line-hover-reverse', + purpose: '折线点悬停与反向弱化', + file: './interaction/line-hover-reverse.mjs' + }, + { + id: 'line-selected-reverse', + purpose: '关闭悬停后折线点选中与反向状态', + file: './interaction/line-selected-reverse.mjs' + }, + { + id: 'radar-selected-reverse', + purpose: '关闭悬停后雷达点选中与反向状态', + file: './interaction/radar-selected-reverse.mjs' + }, + { + id: 'rose-selected-state', + purpose: '关闭悬停后玫瑰扇区选中', + file: './interaction/rose-selected-state.mjs' + }, + { + id: 'legend-clear-selection', + purpose: '图例全部取消后恢复选择(空态检查不截图)', + file: './components/legend/legend-clear-selection.mjs' + }, + { + id: 'legend-single-selection', + purpose: '单选图例切换与数据过滤', + file: './components/legend/legend-single-selection.mjs' + }, + { + id: 'scrollbar-drag-window', + purpose: '拖动横向滚动条改变可视窗口', + file: './components/scrollbar/scrollbar-drag-window.mjs' + }, + { + id: 'scrollbar-drag-axis', + purpose: '拖动滚动条调整轴范围', + file: './components/scrollbar/scrollbar-drag-axis.mjs' + }, + { + id: 'scrollbar-drag-vertical', + purpose: '拖动纵向滚动条改变范围', + file: './components/scrollbar/scrollbar-drag-vertical.mjs' + }, + { + id: 'treemap-selection-persist', + purpose: '矩形树图 triggerOff=none 时空白点击保留选择', + file: './interaction/treemap-selection-persist.mjs' + }, + { + id: 'treemap-label-state', + purpose: '矩形树图标签同步选中状态', + file: './interaction/treemap-label-state.mjs' + }, + { + id: 'funnel-right-align-top', + purpose: '右向漏斗顶部对齐与长外标签', + file: './charts/funnel/funnel-right-align-top.mjs' + }, + { + id: 'funnel-right-align-bottom', + purpose: '右向漏斗底部对齐与长外标签', + file: './charts/funnel/funnel-right-align-bottom.mjs' + }, + { + id: 'funnel-bottom-align-left', + purpose: '下向漏斗左对齐', + file: './charts/funnel/funnel-bottom-align-left.mjs' + }, + { + id: 'funnel-bottom-align-right', + purpose: '下向漏斗右对齐', + file: './charts/funnel/funnel-bottom-align-right.mjs' + }, + { + id: 'funnel-cone-transform', + purpose: '锥形漏斗与转化区域标签', + file: './charts/funnel/funnel-cone-transform.mjs' + }, + { + id: 'progress-vertical-padding', + purpose: '纵向线性进度左右内边距', + file: './charts/progress/progress-vertical-padding.mjs' + }, + { + id: 'progress-threshold-color', + purpose: '线性进度阈值颜色映射', + file: './charts/progress/progress-threshold-color.mjs' + }, + { + id: 'box-plot-outliers', + purpose: '横向箱线图正负异常点', + file: './charts/box-plot/box-plot-outliers.mjs' + }, + { + id: 'line-percent-negative', + purpose: '正负数据百分比堆叠折线', + file: './charts/line/line-percent-negative.mjs' + }, + { + id: 'tooltip-click', + purpose: '点击触发HTML提示框', + file: './components/tooltip/tooltip-click.mjs' + }, + { + id: 'tooltip-trigger-off-none', + purpose: '移出图元后保持提示框', + file: './components/tooltip/tooltip-trigger-off-none.mjs' + }, + { + id: 'tooltip-canvas-click', + purpose: '点击触发Canvas提示框', + file: './components/tooltip/tooltip-canvas-click.mjs' + }, + { + id: 'tooltip-disabled', + purpose: '关闭提示框时鼠标事件仍实际发生', + file: './components/tooltip/tooltip-disabled.mjs' + }, + { + id: 'tooltip-canvas-hover', + purpose: '悬停柱形显示Canvas提示框', + file: './components/tooltip/tooltip-canvas-hover.mjs' + }, + { + id: 'tooltip-title-hidden', + purpose: '隐藏mark提示框标题', + file: './components/tooltip/tooltip-title-hidden.mjs' + }, + { + id: 'tooltip-position-top', + purpose: 'mark提示框顶部定位', + file: './components/tooltip/tooltip-position-top.mjs' + }, + { + id: 'tooltip-position-left', + purpose: 'mark提示框左侧定位', + file: './components/tooltip/tooltip-position-left.mjs' + }, + { + id: 'tooltip-position-right', + purpose: 'mark提示框右侧定位', + file: './components/tooltip/tooltip-position-right.mjs' + }, + { + id: 'tooltip-position-bottom', + purpose: 'mark提示框底部定位', + file: './components/tooltip/tooltip-position-bottom.mjs' + }, + { + id: 'tooltip-position-inside', + purpose: 'mark提示框图元内部定位', + file: './components/tooltip/tooltip-position-inside.mjs' + }, + { + id: 'update-axis-sampling', + purpose: '更新轴 sampling 开关后保留旋转标签', + file: './api/update/update-axis-sampling.mjs' + }, + { + id: 'box-plot-api-hover', + purpose: '分组箱线图公开 API 设置悬停状态', + file: './api/state/box-plot-api-hover.mjs' + }, + { + id: 'update-legend-data', + purpose: '图例选择后更新分类数据', + file: './api/update/update-legend-data.mjs' + }, + { + id: 'sunburst-drill', + purpose: '点击旭日图父节点下钻', + file: './interaction/sunburst-drill.mjs' + }, + { + id: 'funnel-left-align-top', + purpose: '左向漏斗顶部对齐与外标签', + file: './charts/funnel/funnel-left-align-top.mjs' + }, + { + id: 'funnel-left-align-bottom', + purpose: '左向漏斗底部对齐与外标签', + file: './charts/funnel/funnel-left-align-bottom.mjs' + }, + { + id: 'axis-force-tick-count', + purpose: '多个数值轴的 tickCount 与 forceTickCount 组合', + file: './components/axis/axis-force-tick-count.mjs' + }, + { + id: 'range-area-vertical', + purpose: '纵向区间面积的上下界及半透明填充', + file: './charts/range-area/range-area-vertical.mjs' + }, + { + id: 'range-area-average-line', + purpose: '独立数据源的区间面积与平均值折线组合', + file: './charts/range-area/range-area-average-line.mjs' + }, + { + id: 'axis-inside-four-sides', + purpose: '四方向轴的内侧刻度、标签格式与轴线组合', + file: './components/axis/axis-inside-four-sides.mjs' + }, + { + id: 'marker-extension-quadrants', + purpose: '散点象限标区与依赖 region 布局的扩展图元', + file: './components/marker/marker-extension-quadrants.mjs' + }, + { + id: 'marker-richtext-position', + purpose: '起止点富文本标注的尺寸与偏移', + file: './components/richtext/marker-richtext-position.mjs' + }, + { + id: 'axis-time-layer-step', + purpose: '时间轴 layers 的 tickStep 与混合范围数据', + file: './components/axis/axis-time-layer-step.mjs' + }, + { + id: 'axis-tick-count-callback', + purpose: '依据轴长和字体计算数值轴刻度数量', + file: './components/axis/axis-tick-count-callback.mjs' + }, + { + id: 'marker-richtext-content', + purpose: '富文本标注的行内样式与独立末端内容', + file: './components/richtext/marker-richtext-content.mjs' + }, + { + id: 'marker-area-richtext', + purpose: '标区内部富文本标签的排版', + file: './components/richtext/marker-area-richtext.mjs' + }, + { + id: 'marker-coordinate-callbacks', + purpose: '依据轴域和数据计算线、区、点坐标', + file: './components/marker/marker-coordinate-callbacks.mjs' + }, + { + id: 'axis-region-overlap', + purpose: '关联不同系列的 region-relative-overlap 多轴布局', + file: './components/axis/axis-region-overlap.mjs' + }, + { + id: 'bar-stack-sort-min-height', + purpose: '多数据源堆叠排序、逆序与最小柱高', + file: './charts/bar/bar-stack-sort-min-height.mjs' + }, + { + id: 'axis-sync-scrollbar', + purpose: '横向柱图的双数值轴刻度同步与滚动条', + file: './components/axis/axis-sync-scrollbar.mjs' + }, + { + id: 'pie-label-custom-path', + purpose: '外标签引导线自定义折角路径', + file: './components/label/pie-label-custom-path.mjs' + }, + { + id: 'area-mark-hover', + purpose: '面积图点与面积分别悬停的真实状态', + file: './interaction/area-mark-hover.mjs' + }, + { + id: 'tooltip-content-reduce', + purpose: '来源 Tooltip 配置、实际提示内容及移出隐藏', + file: './components/tooltip/tooltip-content-reduce.mjs' + }, + { + id: 'tooltip-dimension-style', + purpose: '来源 Tooltip 配置、实际提示内容及移出隐藏', + file: './components/tooltip/tooltip-dimension-style.mjs' + }, + { + id: 'axis-richtext-tooltip', + purpose: '来源 Tooltip 配置、实际提示内容及移出隐藏', + file: './components/tooltip/axis-richtext-tooltip.mjs' + }, + { + id: 'tooltip-series-dimension', + purpose: '来源 Tooltip 配置、实际提示内容及移出隐藏', + file: './components/tooltip/tooltip-series-dimension.mjs' + }, + { + id: 'axis-poptip-flush', + purpose: '截断轴标签悬停后显示完整文本(源录制 poptip)', + file: './components/axis/axis-poptip-flush.mjs' + }, + { + id: 'axis-poptip-rotated', + purpose: '截断轴标签悬停后显示完整文本(源录制 poptip)', + file: './components/axis/axis-poptip-rotated.mjs' + }, + { + id: 'legend-hover-state', + purpose: '图例悬停反向状态、移出恢复与单选筛选', + file: './components/legend/legend-hover-state.mjs' + }, + { + id: 'legend-hover-no-filter', + purpose: '图例悬停反向状态、移出恢复', + file: './components/legend/legend-hover-no-filter.mjs' + }, + { + id: 'hover-series-api', + purpose: '源 pointerover 回调按 Age 更新同组及反向状态', + file: './interaction/hover-series-api.mjs' + }, + { + id: 'update-full-data-marker', + purpose: 'updateFullDataSync 原始更新值与标记组件', + file: './api/update/update-full-data-marker.mjs' + }, + { + id: 'pie-update-zero', + purpose: '全零饼图经原始 updateData 更新为有效扇区', + file: './api/update/pie-update-zero.mjs' + }, + { + id: 'dimension-index-area', + purpose: 'setDimensionIndex 定位来源指定维度的十字线', + file: './api/state/dimension-index-area.mjs' + }, + { + id: 'custom-mark-text-click', + purpose: '来源 customMark 点击回调修改实际图元', + file: './interaction/custom-mark-text-click.mjs' + }, + { + id: 'custom-mark-fill-click', + purpose: '来源 customMark 点击回调修改实际图元', + file: './interaction/custom-mark-fill-click.mjs' + }, + { + id: 'datazoom-min-max-span', + purpose: '来源 DataZoom 拖动及有效范围约束', + file: './components/datazoom/datazoom-min-max-span.mjs' + }, + { + id: 'datazoom-vertical-fixed-span', + purpose: '来源 DataZoom 拖动及有效范围约束', + file: './components/datazoom/datazoom-vertical-fixed-span.mjs' + }, + { + id: 'datazoom-time-span', + purpose: '来源 DataZoom 拖动及有效范围约束', + file: './components/datazoom/datazoom-time-span.mjs' + }, + { + id: 'datazoom-preview-brush', + purpose: '来源 DataZoom 拖动及有效范围约束', + file: './components/datazoom/datazoom-preview-brush.mjs' + }, + { + id: 'scrollbar-auto', + purpose: '来源滚动条拖动与实际可视范围', + file: './components/scrollbar/scrollbar-auto.mjs' + }, + { + id: 'scrollbar-crosshair', + purpose: '来源滚动条拖动与实际可视范围', + file: './components/scrollbar/scrollbar-crosshair.mjs' + }, + { + id: 'scrollbar-axis-click-update', + purpose: '来源滚动条拖动后点击轴标签更新柱颜色', + file: './components/scrollbar/scrollbar-axis-click-update.mjs' + }, + { + id: 'sankey-selected-adjacency', + purpose: '点击桑基节点时仅高亮相邻节点与连接,空白点击恢复', + file: './charts/sankey/sankey-selected-adjacency.mjs' + }, + { + id: 'sankey-adjacency', + purpose: '点击桑基节点时仅高亮相邻节点与连接,空白点击恢复', + file: './charts/sankey/sankey-adjacency.mjs' + }, + { + id: 'sankey-node-state', + purpose: '来源桑基节点悬停与点击状态', + file: './charts/sankey/sankey-node-state.mjs' + }, + { + id: 'circle-packing-drill-rooted', + purpose: '来源层级图下钻与空白回退的路径和实际布局', + file: './charts/circle-packing/circle-packing-drill-rooted.mjs' + }, + { + id: 'circle-packing-drill-forest', + purpose: '来源层级图下钻与空白回退的路径和实际布局', + file: './charts/circle-packing/circle-packing-drill-forest.mjs' + }, + { + id: 'circle-packing-drill-event', + purpose: '来源层级图下钻与空白回退的路径和实际布局', + file: './charts/circle-packing/circle-packing-drill-event.mjs' + }, + { + id: 'treemap-drill', + purpose: '来源层级图下钻与空白回退的路径和实际布局', + file: './charts/treemap/treemap-drill.mjs' + }, + { + id: 'brush-polygon', + purpose: '来源多边形刷选的实际命中及排除状态', + file: './interaction/brush-polygon.mjs' + }, + { + id: 'brush-region-link', + purpose: '来源跨区域刷选联动的实际命中及排除状态', + file: './interaction/brush-region-link.mjs' + }, + { + id: 'indicator-richtext-click', + purpose: '饼图选择触发富文本指标及对应数值', + file: './components/indicator/indicator-richtext-click.mjs' + }, + { + id: 'pie-label-hover', + purpose: '半圆饼图悬停状态及标签引导线布局', + file: './charts/pie/pie-label-hover.mjs' + }, + { + id: 'funnel-outer-label-line', + purpose: '来源漏斗外标签对齐、虚线与悬停', + file: './charts/funnel/funnel-outer-label-line.mjs' + }, + { + id: 'legend-custom-value', + purpose: '自定义图例值与点击后的真实数据筛选', + file: './components/legend/legend-custom-value.mjs' + }, + { + id: 'crosshair-polar-formatters', + purpose: '极坐标十字线分类和数值 formatter 的真实悬停输出', + file: './components/crosshair/crosshair-polar-formatters.mjs' + }, + { + id: 'theme-runtime-switch', + purpose: '来源主题注册和实例切换后的圆角与配色', + file: './theme/theme-runtime-switch.mjs' + }, + { + id: 'theme-chart-components', + purpose: '来源图表主题启用柱标签并定位左侧图例', + file: './theme/theme-chart-components.mjs' + }, + { + id: 'funnel-extension-select', + purpose: '来源漏斗扩展图元、初始选择和后续点击选择切换', + file: './charts/funnel/funnel-extension-select.mjs' + }, + { + id: 'media-query-width', + purpose: '来源媒体查询注册、缩窄后隐藏左侧坐标轴标签', + file: './layout/media-query-width.mjs' + } +]; + +/** + * 从当前清单所在目录加载用例,冻结副本不回到工作区取代码。 + * @param {CaseMetadata} item + * @returns {Promise} + */ +export async function loadCase(item) { + return (await import(new URL(item.file, import.meta.url))).default; +} diff --git a/packages/vchart/__tests__/visual/cases/interaction/area-mark-hover.mjs b/packages/vchart/__tests__/visual/cases/interaction/area-mark-hover.mjs new file mode 100644 index 0000000000..b3a0ca40c7 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/area-mark-hover.mjs @@ -0,0 +1,71 @@ +import { verifyRendered } from '../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e032f + * 验证目的:面积图点与面积分别悬停的真实状态。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + series: [ + { + type: 'area', + data: { + id: 'data1', + values: [ + { x: 1, y: 80 }, + { x: 2, y: 40 }, + { x: 3, y: 10 }, + { x: 4, y: 20 } + ] + }, + xField: 'x', + yField: 'y', + point: { state: { normal: { fill: 'yellow', size: 30 }, hover: { fill: 'pink' } } }, + area: { + interactive: true, + state: { normal: { cursor: 'pointer', fill: 'grey' }, hover: { fill: 'pink', stroke: '#000' } } + }, + line: { state: { normal: { lineWidth: 20 }, hover: { stroke: 'pink' } } } + } + ], + axes: [{ orient: 'left' }, { orient: 'bottom', type: 'band' }], + tooltip: { transitionDuration: 0 } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const observations = []; + for (const name of ['point', 'area']) { + await page.mouse.move(950, 750); + const p = await interactionTarget(page, name); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + observations.push({ name, states: await seriesStates(page) }); + } + await page.evaluate(v => (window.__observations = v), observations); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await page.evaluate(async () => { + const { assertSpec } = await import('./helpers.mjs'); + const { cases, loadCase } = await import('./cases/index.mjs'); + const expected = (await loadCase(cases.find(c => c.id === 'area-mark-hover'))).createSpec(); + /* VChart 将 line 状态合并到 area;保留来源两份配置,核验合并后的实际结果。 */ expected.series[0].area.state.hover.stroke = + expected.series[0].line.state.hover.stroke; + assertSpec(window.__visualChart.getSpec(), expected); + }); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__observations; + if (r?.length !== 2 || r.some(r => !r.states.some(m => m.name === r.name && m.states.hover > 0))) + throw Error('点或面积未进入 hover'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/area-selected-state.mjs b/packages/vchart/__tests__/visual/cases/interaction/area-selected-state.mjs new file mode 100644 index 0000000000..a2c35d9df4 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/area-selected-state.mjs @@ -0,0 +1,128 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0330 + * 验证目的:面积图数据点和面积区域分别点击后的选择状态。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'common', + series: [ + { + type: 'area', + data: { + id: 'data1', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + hover: true, + select: true, + point: { + state: { + normal: { + fill: 'yellow', + size: 30 + }, + selected: { + fill: 'pink' + } + } + }, + area: { + interactive: true, + state: { + normal: { + cursor: 'pointer', + fill: 'grey' + }, + selected: { + fill: 'pink' + } + } + }, + line: { + state: { + normal: { + lineWidth: 20 + }, + selected: { + stroke: 'pink' + } + } + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom', + type: 'band' + } + ], + tooltip: { + transitionDuration: 0 + } + }; + }, + async exercise(page) { + // 旧坐标缺少宿主尺寸;按源配置的不同图元重建动作,不宣称逐像素重放历史轨迹。 + const observations = []; + for (const name of ['point', 'area']) { + await page.mouse.move(950, 750); + await page.evaluate(() => { + window.__visualChart.clearSelected(); + window.__visualChart.clearHovered(); + }); + const target = await interactionTarget(page, name); + await page.mouse.click(target.x, target.y); + await interactionFrame(page); + observations.push({ target: name, marks: await seriesStates(page) }); + } + await page.evaluate(observations => { + window.__stateObservations = observations; + }, observations); + }, + async verify(page) { + // 每一步都必须命中相应图元,并验证源要求的反向状态;抑制动作后不能通过。 + await verifySpec(page, this.createSpec()); + await page.evaluate( + ({ count, state, reverse }) => { + const rows = window.__stateObservations; + if (!rows || rows.length !== count) throw new Error('缺少多目标交互结果'); + for (const row of rows) { + if (!row.marks.some(m => m.name === row.target && m.states[state] > 0)) + throw new Error('目标图元未进入状态:' + row.target); + if (reverse && !row.marks.some(m => m.states[state + '_reverse'] > 0)) + throw new Error('缺少反向状态:' + row.target); + } + const actual = window.__visualChart.getStage().findAll(g => g.currentStates?.includes(state), true); + if (!actual.length) throw new Error('最终交互状态未保留'); + }, + { count: 2, state: 'selected', reverse: false } + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/brush-polygon.mjs b/packages/vchart/__tests__/visual/cases/interaction/brush-polygon.mjs new file mode 100644 index 0000000000..510137f6af --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/brush-polygon.mjs @@ -0,0 +1,87 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7ef052a1e9eec95f9e88 + * 验证目的:来源多边形刷选的实际命中及排除状态。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'scatter', + data: [ + { + values: [ + { x: 936196, size: 83431, y: 1371, type: '技术', area: '东北' }, + { x: 1270911, size: 219815, y: 5590, type: '办公用品', area: '中南' }, + { x: 453898, size: 19061, y: 727, type: '技术', area: '西南' }, + { x: 919743, size: 148800, y: 1199, type: '家具', area: '华北' }, + { x: 1676224, size: 163453, y: 2517, type: '家具', area: '华东' }, + { x: 1466575, size: 251487, y: 2087, type: '技术', area: '中南' }, + { x: 824673, size: 86067, y: 3622, type: '办公用品', area: '东北' }, + { x: 230956, size: 24016, y: 347, type: '技术', area: '西北' }, + { x: 1599653, size: 228179, y: 2183, type: '技术', area: '华东' }, + { x: 745813, size: 137265, y: 3020, type: '办公用品', area: '华北' }, + { x: 267870, size: 49633, y: 970, type: '办公用品', area: '西北' }, + { x: 1408628, size: 215585, y: 6341, type: '办公用品', area: '华东' }, + { x: 781743, size: 144986, y: 927, type: '技术', area: '华北' }, + { x: 501533, size: 29303, y: 814, type: '家具', area: '西南' }, + { x: 920698, size: 72692, y: 1470, type: '家具', area: '东北' }, + { x: 316212, size: 24903, y: 468, type: '家具', area: '西北' }, + { x: 1399928, size: 199582, y: 2023, type: '家具', area: '中南' }, + { x: 347692, size: 49272, y: 1858, type: '办公用品', area: '西南' } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + sizeField: 'size', + size: [10, 25], + shapeField: 'type', + shape: ['circle', 'triangle'], + axes: [ + { orient: 'left', range: { min: 0 }, type: 'linear' }, + { orient: 'bottom', label: { visible: true }, type: 'linear' } + ], + legends: [ + { + visible: true, + orient: 'left', + position: 'start', + title: { visible: true, style: { text: '标题' } }, + item: { visible: true } + } + ], + direction: 'horizontal', + brush: { brushType: 'polygon', inBrush: { colorAlpha: 1 }, outOfBrush: { colorAlpha: 0.2 } } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'point', 0, 0); + await page.mouse.move(p.x - 15, p.y - 15); + await page.mouse.down(); + await page.mouse.move(p.x + 15, p.y - 15, { steps: 3 }); + await page.mouse.move(p.x + 15, p.y + 15, { steps: 3 }); + await page.mouse.move(p.x - 15, p.y + 15, { steps: 3 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(r => (window.__brushStates = r), await seriesStates(page)); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__brushStates; + if (!r?.some(m => m.states.inBrush > 0) || !r.some(m => m.states.outOfBrush > 0)) + throw Error('刷选未区分实际命中/排除'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/brush-region-link.mjs b/packages/vchart/__tests__/visual/cases/interaction/brush-region-link.mjs new file mode 100644 index 0000000000..f0788472b2 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/brush-region-link.mjs @@ -0,0 +1,182 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 649d7ef252a1e9eec95f9e8a + * 迁移边界:选择三份源数据共同拥有的 x=5;源下方数据从 5 开始,不把其对 x=0 的未命中当作联动失败。 + * 验证目的:来源跨区域刷选联动的实际命中及排除状态。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + stackField: 'stack', + seriesField: 'type', + layout: { + type: 'grid', + col: 3, + row: 5, + elements: [ + { modelId: 'legend', col: 0, row: 4, colSpan: 3 }, + { modelId: 'top', col: 1, row: 0 }, + { modelId: 'middle', col: 1, row: 1 }, + { modelId: 'bottom', col: 1, row: 2 }, + { modelId: 'axesCol0', col: 0, row: 0 }, + { modelId: 'axesCol1', col: 0, row: 1 }, + { modelId: 'axesCol2', col: 2, row: 2 }, + { modelId: 'axesRow', col: 1, row: 3 } + ] + }, + region: [{ id: 'top' }, { id: 'middle' }, { id: 'bottom' }], + legends: { visible: true, orient: 'bottom', id: 'legend', regionIndex: [0, 1, 2] }, + series: [ + { type: 'bar', dataId: 'dataBar', regionIndex: 0, xField: ['x', 'type'], yField: 'y', seriesField: 'color' }, + { type: 'line', dataId: 'dataLine', regionIndex: 1, xField: 'x', yField: 'y', seriesField: 'color' }, + { type: 'bar', dataId: 'dataBar2', regionIndex: 2, xField: ['x', 'type'], yField: 'y', seriesField: 'color' } + ], + axes: [ + { orient: 'left', regionIndex: 0, id: 'axesCol0' }, + { orient: 'left', regionIndex: 1, id: 'axesCol1' }, + { orient: 'right', regionIndex: 2, id: 'axesCol2' }, + { orient: 'bottom', label: { visible: true }, regionIndex: [0, 1, 2], id: 'axesRow' } + ], + brush: { + brushType: 'x', + regionId: 'top', + brushLinkSeriesIndex: [1, 2], + inBrush: { colorAlpha: 1 }, + outOfBrush: { colorAlpha: 0.2 } + }, + data: [ + { + id: 'dataBar', + values: [ + { x: '0', y: '231', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '0', y: '285', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '1', y: '231', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '1', y: '155', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '2', y: '143', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '2', y: '204', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '3', y: '214', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '3', y: '47', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '4', y: '177', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '4', y: '140', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '5', y: '216', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '5', y: '248', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '6', y: '81', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '6', y: '220', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '7', y: '70', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '7', y: '39', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '8', y: '152', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '8', y: '172', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '9', y: '274', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '9', y: '248', y2: '0', stack: 'a', type: 'B', color: 'B' } + ] + }, + { + id: 'dataLine', + values: [ + { x: '0', y: '89', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '0', y: '258', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '1', y: '173', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '1', y: '191', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '2', y: '143', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '2', y: '12', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '3', y: '202', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '3', y: '273', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '4', y: '77', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '4', y: '31', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '5', y: '53', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '5', y: '178', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '6', y: '222', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '6', y: '148', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '7', y: '150', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '7', y: '296', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '8', y: '224', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '8', y: '115', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '9', y: '104', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '9', y: '215', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '10', y: '240', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '10', y: '32', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '11', y: '165', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '11', y: '294', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '12', y: '128', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '12', y: '255', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '13', y: '155', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '13', y: '13', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '14', y: '36', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '14', y: '139', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '15', y: '275', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '15', y: '182', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '16', y: '247', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '16', y: '81', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '17', y: '26', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '17', y: '62', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '18', y: '268', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '18', y: '77', y2: '0', stack: 'a', type: 'B', color: 'D' }, + { x: '19', y: '2', y2: '0', stack: 'a', type: 'A', color: 'C' }, + { x: '19', y: '111', y2: '0', stack: 'a', type: 'B', color: 'D' } + ] + }, + { + id: 'dataBar2', + values: [ + { x: '5', y: '141', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '5', y: '30', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '6', y: '8', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '6', y: '276', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '7', y: '125', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '7', y: '196', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '8', y: '174', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '8', y: '71', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '9', y: '251', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '9', y: '240', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '10', y: '295', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '10', y: '231', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '11', y: '112', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '11', y: '176', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '12', y: '278', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '12', y: '17', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '13', y: '270', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '13', y: '174', y2: '0', stack: 'a', type: 'B', color: 'B' }, + { x: '14', y: '251', y2: '0', stack: 'a', type: 'A', color: 'A' }, + { x: '14', y: '130', y2: '0', stack: 'a', type: 'B', color: 'B' } + ] + } + ], + tooltip: { transitionDuration: 0 } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + const p = await interactionTarget(page, 'bar', 10, 0); + await page.mouse.move(p.x - 15, p.y - 15); + await page.mouse.down(); + await page.mouse.move(p.x + 15, p.y - 15, { steps: 3 }); + await page.mouse.move(p.x + 15, p.y + 15, { steps: 3 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(r => (window.__brushStates = r), await seriesStates(page)); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__brushStates; + if (!r?.some(m => m.states.inBrush > 0) || !r.some(m => m.states.outOfBrush > 0)) + throw Error('刷选未区分实际命中/排除'); + for (const i of [1, 2]) + if ( + !r.some(m => m.series === i && m.states.inBrush > 0) || + !r.some(m => m.series === i && m.states.outOfBrush > 0) + ) + throw Error('刷选没有联动系列 ' + i); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/combination-hover-reverse.mjs b/packages/vchart/__tests__/visual/cases/interaction/combination-hover-reverse.mjs new file mode 100644 index 0000000000..5b4be03c8c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/combination-hover-reverse.mjs @@ -0,0 +1,178 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e0332 + * 验证目的:混合系列悬停与反向弱化状态。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'common', + series: [ + { + type: 'bar', + data: { + id: 'data2', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + bar: { + state: { + normal: { + fill: 'yellow', + size: 30 + }, + hover: { + fill: 'pink' + }, + hover_reverse: { + fill: 'black' + } + } + } + }, + { + type: 'area', + stack: false, + data: { + id: 'data1', + values: [ + { + x: 1, + y: 80 + }, + { + x: 2, + y: 40 + }, + { + x: 3, + y: 10 + }, + { + x: 4, + y: 20 + } + ] + }, + xField: 'x', + yField: 'y', + hover: true, + select: true, + point: { + state: { + normal: { + fill: 'yellow', + size: 30 + }, + hover: { + fill: 'pink' + }, + hover_reverse: { + fill: 'black' + } + } + }, + area: { + interactive: true, + state: { + normal: { + cursor: 'pointer', + fill: 'grey' + }, + hover: { + fill: 'pink' + }, + hover_reverse: { + fill: 'black' + } + } + }, + line: { + state: { + normal: { + lineWidth: 20 + }, + hover: { + stroke: 'pink' + }, + hover_reverse: { + stroke: 'black' + } + } + } + } + ], + axes: [ + { + orient: 'left' + }, + { + orient: 'bottom', + type: 'band' + } + ], + tooltip: { + transitionDuration: 0 + } + }; + }, + async exercise(page) { + // 旧坐标缺少宿主尺寸;按源配置的不同图元重建动作,不宣称逐像素重放历史轨迹。 + const observations = []; + for (const name of ['bar', 'point', 'area']) { + await page.mouse.move(950, 750); + await page.evaluate(() => { + window.__visualChart.clearSelected(); + window.__visualChart.clearHovered(); + }); + const target = await interactionTarget(page, name); + await page.mouse.move(target.x, target.y); + await interactionFrame(page); + observations.push({ target: name, marks: await seriesStates(page) }); + } + await page.evaluate(observations => { + window.__stateObservations = observations; + }, observations); + }, + async verify(page) { + // 每一步都必须命中相应图元,并验证源要求的反向状态;抑制动作后不能通过。 + await verifySpec(page, this.createSpec()); + await page.evaluate( + ({ count, state, reverse }) => { + const rows = window.__stateObservations; + if (!rows || rows.length !== count) throw new Error('缺少多目标交互结果'); + for (const row of rows) { + if (!row.marks.some(m => m.name === row.target && m.states[state] > 0)) + throw new Error('目标图元未进入状态:' + row.target); + if (reverse && !row.marks.some(m => m.states[state + '_reverse'] > 0)) + throw new Error('缺少反向状态:' + row.target); + } + const actual = window.__visualChart.getStage().findAll(g => g.currentStates?.includes(state), true); + if (!actual.length) throw new Error('最终交互状态未保留'); + }, + { count: 3, state: 'hover', reverse: true } + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/custom-mark-click.mjs b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-click.mjs new file mode 100644 index 0000000000..89b3eefa97 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-click.mjs @@ -0,0 +1,233 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 66ed4970ded41300eeb7ff68 + * 验证目的:按 markName 绑定的自定义图元点击更新。 + * 迁移说明:保留源数据、计算和回调;翻译通用显示文字,适配本地宿主。 + * 覆盖边界:保留来源动作或按源事件执行操作并检查结果。 + */ +export default { + createSpec() { + // 在每次调用中重建原始配置及数据,保留来源中的确定性计算。 + const spec = { + type: 'line', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + Age: 'Under 5 Years', + Population: 25635 + }, + { + State: 'WY', + Age: '5 to 13 Years', + Population: 1890 + }, + { + State: 'WY', + Age: '14 to 17 Years', + Population: 9314 + }, + { + State: 'DC', + Age: 'Under 5 Years', + Population: 30352 + }, + { + State: 'DC', + Age: '5 to 13 Years', + Population: 20439 + }, + { + State: 'DC', + Age: '14 to 17 Years', + Population: 10225 + }, + { + State: 'VT', + Age: 'Under 5 Years', + Population: 38253 + }, + { + State: 'VT', + Age: '5 to 13 Years', + Population: 42538 + }, + { + State: 'VT', + Age: '14 to 17 Years', + Population: 15757 + }, + { + State: 'ND', + Age: 'Under 5 Years', + Population: 51896 + }, + { + State: 'ND', + Age: '5 to 13 Years', + Population: 67358 + }, + { + State: 'ND', + Age: '14 to 17 Years', + Population: 18794 + }, + { + State: 'AK', + Age: 'Under 5 Years', + Population: 72083 + }, + { + State: 'AK', + Age: '5 to 13 Years', + Population: 85640 + }, + { + State: 'AK', + Age: '14 to 17 Years', + Population: 22153 + } + ] + } + ], + customMark: [ + { + type: 'rect', + name: 'top-rect', + layoutType: 'region-relative', + orient: 'top', + height: 40, + visible: true, + zIndex: 10001, + style: { + fill: '#c9c9c9', + x: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.x1; + }, + + y: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.y1; + }, + width: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.width(); + }, + height: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.height(); + } + } + }, + { + type: 'rect', + name: 'bottom-rect', + layoutType: 'region-relative', + orient: 'bottom', + height: 20, + visible: true, + zIndex: 10001, + style: { + fill: '#e8e8e8', + x: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + // return (bounds.x1 + bounds.x2) / 2; + return bounds.x1; + }, + + y: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.y1; + }, + width: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.width(); + }, + height: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + + return bounds.height(); + } + } + } + ], + xField: 'State', + yField: 'Population', + seriesField: 'Age', + legends: { + visible: true + }, + point: { + style: { + size: 0, + fill: 'white', + stroke: null, + lineWidth: 2 + }, + state: { + dimension_hover: { + size: 8 + }, + + unHover_line: { + size: 0 + } + } + }, + line: { + state: { + hover_line: { + strokeOpacity: 1 + }, + unHover_line: { + strokeOpacity: 0.1 + } + } + }, + crosshair: { + xField: { + line: { + type: 'line' + } + } + } + }; + + return spec; + }, + async exercise(page) { + // 适配源动作;鼠标定位按实际图元,避免绑定旧宿主像素坐标。 + await page.evaluate(() => + window.__visualChart.on('click', { markName: 'top-rect' }, e => e.event.target.setAttributes({ fill: 'red' })) + ); + const point = await page.evaluate(() => { + const m = window.__visualChart + .getChart() + .getComponentsByType('customMark') + .flatMap(c => c.getMarks()) + .find(m => m.name === 'top-rect'); + const b = m.getGraphics()[0].globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(point.x, point.y); + await page.mouse.move(950, 750); + }, + async verify(page) { + // 完整配置和回调由 verifySourceSpec 检查,另验证最终绘制或操作结果。 + await verifySourceSpec(page); + await verifyRendered(page, 'series'); + await page.waitForFunction( + () => + window.__visualChart.getStage().findAll(g => g.type === 'rect' && g.attribute.fill === 'red', true).length > 0 + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/custom-mark-fill-click.mjs b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-fill-click.mjs new file mode 100644 index 0000000000..1fe2263718 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-fill-click.mjs @@ -0,0 +1,140 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionFrame } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 66ed43a315288000e7958d90 + * 验证目的:来源 customMark 点击回调修改实际图元。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'line', + data: [ + { + id: 'barData', + values: [ + { State: 'WY', Age: 'Under 5 Years', Population: 25635 }, + { State: 'WY', Age: '5 to 13 Years', Population: 1890 }, + { State: 'WY', Age: '14 to 17 Years', Population: 9314 }, + { State: 'DC', Age: 'Under 5 Years', Population: 30352 }, + { State: 'DC', Age: '5 to 13 Years', Population: 20439 }, + { State: 'DC', Age: '14 to 17 Years', Population: 10225 }, + { State: 'VT', Age: 'Under 5 Years', Population: 38253 }, + { State: 'VT', Age: '5 to 13 Years', Population: 42538 }, + { State: 'VT', Age: '14 to 17 Years', Population: 15757 }, + { State: 'ND', Age: 'Under 5 Years', Population: 51896 }, + { State: 'ND', Age: '5 to 13 Years', Population: 67358 }, + { State: 'ND', Age: '14 to 17 Years', Population: 18794 }, + { State: 'AK', Age: 'Under 5 Years', Population: 72083 }, + { State: 'AK', Age: '5 to 13 Years', Population: 85640 }, + { State: 'AK', Age: '14 to 17 Years', Population: 22153 } + ] + } + ], + customMark: [ + { + type: 'rect', + layoutType: 'region-relative', + orient: 'top', + height: 40, + visible: true, + zIndex: 10001, + style: { + fill: '#c9c9c9', + x: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.x1; + }, + y: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.y1; + }, + width: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.width(); + }, + height: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.height(); + } + } + }, + { + type: 'rect', + layoutType: 'region-relative', + orient: 'bottom', + height: 20, + visible: true, + zIndex: 10001, + style: { + fill: '#e8e8e8', + x: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.x1; + }, + y: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.y1; + }, + width: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.width(); + }, + height: (datum, ctx) => { + const bounds = ctx.getLayoutBounds(); + return bounds.height(); + } + } + } + ], + xField: 'State', + yField: 'Population', + seriesField: 'Age', + legends: { visible: true }, + point: { + style: { size: 0, fill: 'white', stroke: null, lineWidth: 2 }, + state: { dimension_hover: { size: 8 }, unHover_line: { size: 0 } } + }, + line: { state: { hover_line: { strokeOpacity: 1 }, unHover_line: { strokeOpacity: 0.1 } } }, + crosshair: { xField: { line: { type: 'line' } } } + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.on('click', { level: 'model', type: 'customMark' }, e => { + const target = e.event.target; + target.setAttributes({ fill: 'red' }); + }); + }); + const p = await page.evaluate(() => { + const c = window.__visualChart; + const group = c + .getChart() + .getAllComponents() + .find(c => c.type === 'customMark') + .getMarks()[0] + .getGraphics()[0]; + const g = group.type === 'group' ? group.find(g => g.type !== 'group', true) : group; + const b = g.globalAABBBounds; + window.__customTarget = g; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (window.__customTarget?.attribute.fill !== 'red') throw Error('customMark 点击回调未修改图元'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/custom-mark-text-click.mjs b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-text-click.mjs new file mode 100644 index 0000000000..c0d563c157 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/custom-mark-text-click.mjs @@ -0,0 +1,122 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionFrame } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65eedea7e927e800ae5d8de0 + * 验证目的:来源 customMark 点击回调修改实际图元。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + data: [ + { + id: 'barData', + values: [ + { date: '第一天', class: 'A', score: 20, highest: true }, + { date: '第一天', class: 'B', score: 18 }, + { date: '第一天', class: 'C', score: 19 }, + { date: '第一天', class: 'D', score: 15 }, + { date: '第二天', class: 'A', score: 16 }, + { date: '第二天', class: 'B', score: 19, highest: true }, + { date: '第二天', class: 'C', score: 18 }, + { date: '第二天', class: 'D', score: 14 }, + { date: '第三天', class: 'A', score: 19, highest: true }, + { date: '第三天', class: 'B', score: 16 }, + { date: '第三天', class: 'C', score: 18 }, + { date: '第三天', class: 'D', score: 13 }, + { date: '第四天', class: 'A', score: 18 }, + { date: '第四天', class: 'B', score: 20 }, + { date: '第四天', class: 'C', score: 22 }, + { date: '第四天', class: 'D', score: 26, highest: true } + ] + } + ], + series: [ + { + type: 'bar', + yField: 'score', + xField: ['date', 'class'], + seriesField: 'class', + extensionMark: [ + { + type: 'symbol', + dataId: 'barData', + visible: true, + zIndex: 500, + style: { + symbolType: + 'M134.404 233.406C131.29 211.4 146.482 191.016 168.334 187.88c21.852-3.136 42.092 12.16 45.206 34.17l91.124 644.016c3.114 22.008-12.076 42.39-33.928 45.526-21.854 3.136-42.092-12.162-45.206-34.17L134.404 233.406zM637.48 287.434c-47.104 5.628-97.372 0.908-168.788-12.58-11.554-2.18-22.494-4.346-40.062-7.894-44.648-9.018-57.702-11.546-77.862-14.79-26.226-4.22-47.57-6.34-65.826-6.34-22.23 0-40.25-18.034-40.25-40.28 0-22.244 18.02-40.278 40.25-40.278 23.156 0 48.506 2.52 78.608 7.364 21.64 3.482 35.172 6.1 81.006 15.36 17.26 3.484 27.926 5.596 39.064 7.7 63.93 12.072 107.426 16.158 144.314 11.75 64.332-7.686 129.64-36.462 196.136-87.112 28.106-21.41 68.172 1.206 64.4 36.352l-20.932 194.948a40.3 40.3 0 0 1-3.698 13.052c-62.652 131.306-150.578 203.8-262.236 211.334-84 5.668-154.226 44.084-212.82 117.14-13.916 17.346-39.25 20.122-56.586 6.2-17.336-13.926-20.11-39.278-6.194-56.626 72.352-90.206 163.13-139.866 270.184-147.088 77.876-5.254 141.41-56.51 192.058-159.576l11.078-103.188c-53.446 29.78-107.416 48.048-161.846 54.552z', + x: (datum, ctx, elements, dataView) => { + return ctx.valueToX([datum.date, datum.class]); + }, + y: (datum, ctx, elements, dataView) => { + return ctx.valueToY([datum.score]); + }, + fill: 'red', + size: 20, + visible: datum => datum.highest === true + } + } + ] + } + ], + axes: [ + { orient: 'left', type: 'linear', nice: true, range: { max: 30 }, grid: { style: { stroke: 'red' } } }, + { orient: 'bottom', type: 'band' } + ], + customMark: [ + { + type: 'text', + zIndex: 10, + id: 'description', + style: { + textBaseline: 'top', + fontSize: 200, + textAlign: 'start', + fontFamily: 'PingFang SC', + fontWeight: 600, + text: 2345, + x: 100, + y: 100, + fill: 'grey' + } + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.on('click', { level: 'model', type: 'customMark' }, e => { + const group = chartSpace.getStage().getElementsByName('description')[0]; + const text = group.getElementsByType('text')[0]; + text.setAttribute('text', 'ssss'); + }); + }); + const p = await page.evaluate(() => { + const c = window.__visualChart; + const group = c.getStage().getElementsByName('description')[0], + g = group.getElementsByType('text')[0]; + const b = g.globalAABBBounds; + window.__customTarget = g; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }); + await page.mouse.click(p.x, p.y); + await page.mouse.move(950, 750); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + if (window.__customTarget?.attribute.text !== 'ssss') throw Error('customMark 点击回调未修改图元'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/hover-series-api.mjs b/packages/vchart/__tests__/visual/cases/interaction/hover-series-api.mjs new file mode 100644 index 0000000000..23ad3089f3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/hover-series-api.mjs @@ -0,0 +1,122 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 65969dc589f0d500a1fdd476 + * 验证目的:源 pointerover 回调按 Age 更新同组及反向状态。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { State: 'WY', Age: 'Under 5 Years', Population: 25635 }, + { State: 'WY', Age: '5 to 13 Years', Population: 1890 }, + { State: 'WY', Age: '14 to 17 Years', Population: 9314 }, + { State: 'DC', Age: 'Under 5 Years', Population: 30352 }, + { State: 'DC', Age: '5 to 13 Years', Population: 20439 }, + { State: 'DC', Age: '14 to 17 Years', Population: 10225 }, + { State: 'VT', Age: 'Under 5 Years', Population: 38253 }, + { State: 'VT', Age: '5 to 13 Years', Population: 42538 }, + { State: 'VT', Age: '14 to 17 Years', Population: 15757 }, + { State: 'ND', Age: 'Under 5 Years', Population: 51896 }, + { State: 'ND', Age: '5 to 13 Years', Population: 67358 }, + { State: 'ND', Age: '14 to 17 Years', Population: 18794 }, + { State: 'AK', Age: 'Under 5 Years', Population: 72083 }, + { State: 'AK', Age: '5 to 13 Years', Population: 85640 }, + { State: 'AK', Age: '14 to 17 Years', Population: 22153 } + ] + } + ], + yField: 'State', + xField: 'Population', + seriesField: 'Age', + direction: 'horizontal', + stack: true, + percent: true, + legends: { visible: true }, + label: { + visible: true, + position: 'center', + smartInvert: true, + syncState: true, + style: { fill: '#222', stroke: null }, + state: { hover_series: { opacity: 1 }, unHover_series: { opacity: 0.1 } } + }, + bar: { state: { hover_series: { opacity: 1 }, unHover_series: { opacity: 0.2 } } }, + tooltip: { transitionDuration: 0 }, + axes: [ + { + orient: 'top', + label: { + formatMethod: val => { + return `${(val * 100).toFixed(2)}%`; + } + } + } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(() => { + const chartSpace = window.__visualChart; + chartSpace.on('pointerover', { level: 'mark' }, ({ datum, mark }) => { + if (mark?.name === 'bar' && datum) { + const series = datum.Age; + chartSpace.updateState({ + hover_series: { + filter: datum => datum.Age === series + }, + unHover_series: { + filter: datum => datum.Age !== series + } + }); + } + }); + chartSpace.on('pointerout', { level: 'mark' }, ({ datum, mark }) => { + if (mark?.name === 'bar' && datum) { + chartSpace.updateState({ + hover_series: { + filter: () => false + }, + unHover_series: { + filter: () => false + } + }); + } + }); + }); + const p = await interactionTarget(page, 'bar'); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + const hover = await seriesStates(page); + await page.mouse.move(950, 750); + await interactionFrame(page); + const off = await seriesStates(page); + await page.mouse.move(p.x, p.y); + await interactionFrame(page); + await page.evaluate(v => (window.__apiStates = v), { hover, off }); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const r = window.__apiStates; + if ( + !r?.hover.some(m => m.states.hover_series > 0) || + !r.hover.some(m => m.states.unHover_series > 0) || + r.off.some(m => m.states.hover_series > 0 || m.states.unHover_series > 0) + ) + throw Error('自定义系列 hover/filter 未生效'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/line-hover-reverse.mjs b/packages/vchart/__tests__/visual/cases/interaction/line-hover-reverse.mjs new file mode 100644 index 0000000000..79eab5a049 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/line-hover-reverse.mjs @@ -0,0 +1,212 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03ce + * 验证目的:折线点悬停与反向弱化。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '898' + }, + { + x: '1', + type: 'A', + y: '813' + }, + { + x: '2', + type: 'A', + y: '696' + }, + { + x: '3', + type: 'A', + y: '799' + }, + { + x: '4', + type: 'A', + y: '846' + }, + { + x: '5', + type: 'A', + y: '847' + }, + { + x: '6', + type: 'A', + y: '609' + }, + { + x: '7', + type: 'A', + y: '742' + }, + { + x: '8', + type: 'A', + y: '650' + }, + { + x: '9', + type: 'A', + y: '712' + }, + { + x: '0', + type: 'B', + y: '880' + }, + { + x: '1', + type: 'B', + y: '814' + }, + { + x: '2', + type: 'B', + y: '746' + }, + { + x: '3', + type: 'B', + y: '846' + }, + { + x: '4', + type: 'B', + y: '760' + }, + { + x: '5', + type: 'B', + y: '643' + }, + { + x: '6', + type: 'B', + y: '881' + }, + { + x: '7', + type: 'B', + y: '836' + }, + { + x: '8', + type: 'B', + y: '715' + }, + { + x: '9', + type: 'B', + y: '647' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6, + visible: false + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + line: { + state: { + hover: { + lineWidth: 4 + }, + selected: { + stroke: 'red' + }, + hover_reverse: { + stroke: '#ddd' + } + } + }, + tooltip: { + transitionDuration: 0 + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + hover_reverse: { + fill: '#ddd' + } + } + } + }; + }, + async exercise(page) { + // 旧坐标缺少宿主尺寸;按源配置的不同图元重建动作,不宣称逐像素重放历史轨迹。 + const observations = []; + for (const name of ['point', 'line']) { + await page.mouse.move(950, 750); + await page.evaluate(() => { + window.__visualChart.clearSelected(); + window.__visualChart.clearHovered(); + }); + const target = await interactionTarget(page, name); + await page.mouse.move(target.x, target.y); + await interactionFrame(page); + observations.push({ target: name, marks: await seriesStates(page) }); + } + await page.evaluate(observations => { + window.__stateObservations = observations; + }, observations); + }, + async verify(page) { + // 每一步都必须命中相应图元,并验证源要求的反向状态;抑制动作后不能通过。 + await verifySpec(page, this.createSpec()); + await page.evaluate( + ({ count, state, reverse }) => { + const rows = window.__stateObservations; + if (!rows || rows.length !== count) throw new Error('缺少多目标交互结果'); + for (const row of rows) { + if (!row.marks.some(m => m.name === row.target && m.states[state] > 0)) + throw new Error('目标图元未进入状态:' + row.target); + if (reverse && !row.marks.some(m => m.states[state + '_reverse'] > 0)) + throw new Error('缺少反向状态:' + row.target); + } + const actual = window.__visualChart.getStage().findAll(g => g.currentStates?.includes(state), true); + if (!actual.length) throw new Error('最终交互状态未保留'); + }, + { count: 2, state: 'hover', reverse: true } + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/line-selected-reverse.mjs b/packages/vchart/__tests__/visual/cases/interaction/line-selected-reverse.mjs new file mode 100644 index 0000000000..4c42762258 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/line-selected-reverse.mjs @@ -0,0 +1,213 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03cf + * 验证目的:关闭悬停后折线点选中与反向状态。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'line', + data: [ + { + id: 'id0', + values: [ + { + x: '0', + type: 'A', + y: '898' + }, + { + x: '1', + type: 'A', + y: '813' + }, + { + x: '2', + type: 'A', + y: '696' + }, + { + x: '3', + type: 'A', + y: '799' + }, + { + x: '4', + type: 'A', + y: '846' + }, + { + x: '5', + type: 'A', + y: '847' + }, + { + x: '6', + type: 'A', + y: '609' + }, + { + x: '7', + type: 'A', + y: '742' + }, + { + x: '8', + type: 'A', + y: '650' + }, + { + x: '9', + type: 'A', + y: '712' + }, + { + x: '0', + type: 'B', + y: '880' + }, + { + x: '1', + type: 'B', + y: '814' + }, + { + x: '2', + type: 'B', + y: '746' + }, + { + x: '3', + type: 'B', + y: '846' + }, + { + x: '4', + type: 'B', + y: '760' + }, + { + x: '5', + type: 'B', + y: '643' + }, + { + x: '6', + type: 'B', + y: '881' + }, + { + x: '7', + type: 'B', + y: '836' + }, + { + x: '8', + type: 'B', + y: '715' + }, + { + x: '9', + type: 'B', + y: '647' + } + ] + } + ], + xField: 'x', + yField: 'y', + seriesField: 'type', + hover: false, + axes: [ + { + orient: 'left', + range: { + min: 500 + }, + expand: { + max: 0.2 + }, + tickCount: 6, + forceTickCount: 6, + visible: false + }, + { + orient: 'bottom', + label: { + visible: true + } + } + ], + line: { + state: { + hover: { + strokeWidth: 4 + }, + selected: { + stroke: 'yellow' + }, + selected_reverse: { + stroke: '#ddd' + } + } + }, + tooltip: { + transitionDuration: 0 + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + selected_reverse: { + fill: '#ddd' + } + } + } + }; + }, + async exercise(page) { + // 旧坐标缺少宿主尺寸;按源配置的不同图元重建动作,不宣称逐像素重放历史轨迹。 + const observations = []; + for (const name of ['point', 'line']) { + await page.mouse.move(950, 750); + await page.evaluate(() => { + window.__visualChart.clearSelected(); + window.__visualChart.clearHovered(); + }); + const target = await interactionTarget(page, name); + await page.mouse.click(target.x, target.y); + await interactionFrame(page); + observations.push({ target: name, marks: await seriesStates(page) }); + } + await page.evaluate(observations => { + window.__stateObservations = observations; + }, observations); + }, + async verify(page) { + // 每一步都必须命中相应图元,并验证源要求的反向状态;抑制动作后不能通过。 + await verifySpec(page, this.createSpec()); + await page.evaluate( + ({ count, state, reverse }) => { + const rows = window.__stateObservations; + if (!rows || rows.length !== count) throw new Error('缺少多目标交互结果'); + for (const row of rows) { + if (!row.marks.some(m => m.name === row.target && m.states[state] > 0)) + throw new Error('目标图元未进入状态:' + row.target); + if (reverse && !row.marks.some(m => m.states[state + '_reverse'] > 0)) + throw new Error('缺少反向状态:' + row.target); + } + const actual = window.__visualChart.getStage().findAll(g => g.currentStates?.includes(state), true); + if (!actual.length) throw new Error('最终交互状态未保留'); + }, + { count: 2, state: 'selected', reverse: true } + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/radar-selected-reverse.mjs b/packages/vchart/__tests__/visual/cases/interaction/radar-selected-reverse.mjs new file mode 100644 index 0000000000..db4b22f24c --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/radar-selected-reverse.mjs @@ -0,0 +1,234 @@ +import { interactionTarget, interactionFrame, seriesStates } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03d3 + * 验证目的:关闭悬停后雷达点选中与反向状态。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'radar', + data: { + id: 'radar', + values: [ + { + theta: '0', + type: 'A', + r: '873' + }, + { + theta: '1', + type: 'A', + r: '713' + }, + { + theta: '2', + type: 'A', + r: '742' + }, + { + theta: '3', + type: 'A', + r: '861' + }, + { + theta: '4', + type: 'A', + r: '815' + }, + { + theta: '5', + type: 'A', + r: '660' + }, + { + theta: '6', + type: 'A', + r: '795' + }, + { + theta: '7', + type: 'A', + r: '722' + }, + { + theta: '8', + type: 'A', + r: '818' + }, + { + theta: '9', + type: 'A', + r: '666' + }, + { + theta: '0', + type: 'B', + r: '896' + }, + { + theta: '1', + type: 'B', + r: '814' + }, + { + theta: '2', + type: 'B', + r: '856' + }, + { + theta: '3', + type: 'B', + r: '840' + }, + { + theta: '4', + type: 'B', + r: '634' + }, + { + theta: '5', + type: 'B', + r: '616' + }, + { + theta: '6', + type: 'B', + r: '652' + }, + { + theta: '7', + type: 'B', + r: '831' + }, + { + theta: '8', + type: 'B', + r: '614' + }, + { + theta: '9', + type: 'B', + r: '648' + } + ] + }, + categoryField: 'theta', + valueField: 'r', + seriesField: 'type', + groupBy: 'type', + hover: false, + line: { + style: { + lineWidth: 8 + }, + state: { + hover: { + stroke: 'red' + }, + selected: { + lineWidth: 10 + }, + selected_reverse: { + stroke: '#ccc' + } + } + }, + point: { + state: { + hover: { + fill: 'red' + }, + selected: { + fill: 'yellow' + }, + selected_reverse: { + fill: 'blue' + } + } + }, + startAngle: 90, + tooltip: { + transitionDuration: 0 + }, + axes: [ + { + orient: 'angle', + domainLine: { + visible: true, + smooth: false + }, + tick: { + visible: true + }, + grid: { + visible: true + }, + label: { + visible: true + } + }, + { + orient: 'radius', + label: { + visible: true + }, + domainLine: { + visible: true, + smooth: false + }, + tick: { + visible: true + }, + grid: { + visible: true, + smooth: false, + style: { + stroke: 'red' + } + } + } + ] + }; + }, + async exercise(page) { + // 旧坐标缺少宿主尺寸;按源配置的不同图元重建动作,不宣称逐像素重放历史轨迹。 + const observations = []; + for (const name of ['point', 'line']) { + await page.mouse.move(950, 750); + await page.evaluate(() => { + window.__visualChart.clearSelected(); + window.__visualChart.clearHovered(); + }); + const target = await interactionTarget(page, name); + await page.mouse.click(target.x, target.y); + await interactionFrame(page); + observations.push({ target: name, marks: await seriesStates(page) }); + } + await page.evaluate(observations => { + window.__stateObservations = observations; + }, observations); + }, + async verify(page) { + // 每一步都必须命中相应图元,并验证源要求的反向状态;抑制动作后不能通过。 + await verifySpec(page, this.createSpec()); + await page.evaluate( + ({ count, state, reverse }) => { + const rows = window.__stateObservations; + if (!rows || rows.length !== count) throw new Error('缺少多目标交互结果'); + for (const row of rows) { + if (!row.marks.some(m => m.name === row.target && m.states[state] > 0)) + throw new Error('目标图元未进入状态:' + row.target); + if (reverse && !row.marks.some(m => m.states[state + '_reverse'] > 0)) + throw new Error('缺少反向状态:' + row.target); + } + const actual = window.__visualChart.getStage().findAll(g => g.currentStates?.includes(state), true); + if (!actual.length) throw new Error('最终交互状态未保留'); + }, + { count: 2, state: 'selected', reverse: true } + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/rose-selected-state.mjs b/packages/vchart/__tests__/visual/cases/interaction/rose-selected-state.mjs new file mode 100644 index 0000000000..e9131dc63e --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/rose-selected-state.mjs @@ -0,0 +1,111 @@ +import { verifySpec, seriesGraphicCenter } from '../../helpers.mjs'; +/** + * BugServer case IDs: 646745e9cb5fa8011f4e03d5 + * 验证目的:关闭悬停后玫瑰扇区选中。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'rose', + data: [ + { + id: 'id0', + values: [ + { + value: '886', + type: 'A', + stack: 'stack0' + }, + { + value: '856', + type: 'B', + stack: 'stack1' + }, + { + value: '839', + type: 'C', + stack: 'stack2' + }, + { + value: '755', + type: 'D', + stack: 'stack3' + }, + { + value: '797', + type: 'E', + stack: 'stack4' + } + ] + } + ], + radius: 0.8, + categoryField: 'type', + valueField: 'value', + seriesField: 'stack', + axes: [ + { + orient: 'angle', + domainLine: { + visible: true, + smooth: false + }, + tick: { + visible: true + }, + grid: { + visible: true + }, + label: { + visible: true + } + }, + { + orient: 'radius', + label: { + visible: true + }, + grid: { + visible: true, + smooth: false + }, + domainLine: { + visible: true, + smooth: false + }, + tick: { + visible: true + } + } + ], + hover: false, + rose: { + state: { + hover: { + fill: 'red' + }, + selected: { + stroke: '#000', + lineWidth: 4 + } + } + } + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + const p = await seriesGraphicCenter(page, 'rose'); + await page.mouse.click(p.x, p.y); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const all = window.__visualChart.getStage().findAll(g => g.currentStates?.includes('selected'), true); + return all.length > 0; + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/sunburst-drill.mjs b/packages/vchart/__tests__/visual/cases/interaction/sunburst-drill.mjs new file mode 100644 index 0000000000..430958f8d7 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/sunburst-drill.mjs @@ -0,0 +1,514 @@ +import { interactionFrame } from '../../interaction-helpers.mjs'; +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +/** + * BugServer case IDs: 649d7f2152a1e9eec95f9f11 + * 验证目的:点击旭日图父节点下钻。 + * 保留条件:源数据、配置、确定性回调;宿主及通用文字适配本地。 + * 覆盖边界:仅验证指定最终状态,不覆盖动画过程或全部录制步骤。 + */ +export default { + createSpec() { + // 返回独立源配置,保持数据顺序及计算关系。 + const data = [ + { + name: 'query', + children: [ + { + name: 'methods', + children: [ + { name: 'add', value: 593 }, + { name: 'and', value: 330 }, + { name: 'average', value: 287 }, + { name: 'count', value: 277 }, + { name: 'distinct', value: 292 }, + { name: 'div', value: 595 }, + { name: 'eq', value: 594 }, + { name: 'fn', value: 460 }, + { name: 'gt', value: 603 }, + { name: 'gte', value: 625 }, + { name: 'iff', value: 748 }, + { name: 'isa', value: 461 }, + { name: 'lt', value: 597 }, + { name: 'lte', value: 619 }, + { name: 'max', value: 283 }, + { name: 'min', value: 283 }, + { name: 'mod', value: 591 }, + { name: 'mul', value: 603 }, + { name: 'neq', value: 599 }, + { name: 'not', value: 386 }, + { name: 'or', value: 323 }, + { name: 'orderby', value: 307 }, + { name: 'range', value: 772 }, + { name: 'select', value: 296 }, + { name: 'stddev', value: 363 }, + { name: 'sub', value: 600 }, + { name: 'sum', value: 280 }, + { name: 'update', value: 307 }, + { name: 'variance', value: 335 }, + { name: 'where', value: 299 }, + { name: 'xor', value: 354 }, + { name: '_', value: 264 } + ] + }, + { name: 'AggregateExpression', value: 1616 }, + { name: 'And', value: 1027 }, + { name: 'Arithmetic', value: 3891 }, + { name: 'Average', value: 891 }, + { name: 'BinaryExpression', value: 2893 }, + { name: 'Comparison', value: 5103 }, + { name: 'CompositeExpression', value: 3677 }, + { name: 'Count', value: 781 }, + { name: 'DateUtil', value: 4141 }, + { name: 'Distinct', value: 933 }, + { name: 'Expression', value: 5130 }, + { name: 'ExpressionIterator', value: 3617 }, + { name: 'Fn', value: 3240 }, + { name: 'If', value: 2732 }, + { name: 'IsA', value: 2039 }, + { name: 'Literal', value: 1214 }, + { name: 'Match', value: 3748 }, + { name: 'Maximum', value: 843 }, + { name: 'Minimum', value: 843 }, + { name: 'Not', value: 1554 }, + { name: 'Or', value: 970 }, + { name: 'Query', value: 13896 }, + { name: 'Range', value: 1594 }, + { name: 'StringUtil', value: 4130 }, + { name: 'Sum', value: 791 }, + { name: 'Variable', value: 1124 }, + { name: 'Variance', value: 1876 }, + { name: 'Xor', value: 1101 } + ] + }, + { + name: 'util', + children: [ + { + name: 'palette', + children: [ + { name: 'ColorPalette', value: 6367 }, + { name: 'Palette', value: 1229 }, + { name: 'ShapePalette', value: 2059 }, + { name: 'valuePalette', value: 2291 } + ] + }, + { + name: 'math', + children: [ + { name: 'DenseMatrix', value: 3165 }, + { name: 'IMatrix', value: 2815 }, + { name: 'SparseMatrix', value: 3366 } + ] + }, + { + name: 'heap', + children: [ + { name: 'FibonacciHeap', value: 9354 }, + { name: 'HeapNode', value: 1233 } + ] + }, + { name: 'Arrays', value: 8258 }, + { name: 'Colors', value: 10001 }, + { name: 'Dates', value: 8217 }, + { name: 'Displays', value: 12555 }, + { name: 'Filter', value: 2324 }, + { name: 'Geometry', value: 10993 }, + { name: 'IEvaluable', value: 335 }, + { name: 'IPredicate', value: 383 }, + { name: 'IValueProxy', value: 874 }, + { name: 'Maths', value: 17705 }, + { name: 'Orientation', value: 1486 }, + { name: 'Property', value: 5559 }, + { name: 'Shapes', value: 19118 }, + { name: 'Sort', value: 6887 }, + { name: 'Stats', value: 6557 }, + { name: 'Strings', value: 22026 } + ] + }, + { + name: 'animate', + children: [ + { + name: 'interpolate', + children: [ + { name: 'ArrayInterpolator', value: 1983 }, + { name: 'ColorInterpolator', value: 2047 }, + { name: 'DateInterpolator', value: 1375 }, + { name: 'Interpolator', value: 8746 }, + { name: 'MatrixInterpolator', value: 2202 }, + { name: 'NumberInterpolator', value: 1382 }, + { name: 'ObjectInterpolator', value: 1629 }, + { name: 'PointInterpolator', value: 1675 }, + { name: 'RectangleInterpolator', value: 2042 } + ] + }, + { name: 'Easing', value: 17010 }, + { name: 'FunctionSequence', value: 5842 }, + { name: 'ISchedulable', value: 1041 }, + { name: 'Parallel', value: 5176 }, + { name: 'Pause', value: 449 }, + { name: 'Scheduler', value: 5593 }, + { name: 'Sequence', value: 5534 }, + { name: 'Transition', value: 9201 }, + { name: 'Transitioner', value: 19975 }, + { name: 'TransitionEvent', value: 1116 }, + { name: 'Tween', value: 6006 } + ] + }, + { + name: 'scale', + children: [ + { name: 'IScaleMap', value: 2105 }, + { name: 'LinearScale', value: 1316 }, + { name: 'LogScale', value: 3151 }, + { name: 'OrdinalScale', value: 3770 }, + { name: 'QuantileScale', value: 2435 }, + { name: 'QuantitativeScale', value: 4839 }, + { name: 'RootScale', value: 1756 }, + { name: 'Scale', value: 4268 }, + { name: 'ScaleType', value: 1821 }, + { name: 'TimeScale', value: 5833 } + ] + }, + { + name: 'physics', + children: [ + { name: 'DragForce', value: 1082 }, + { name: 'GravityForce', value: 1336 }, + { name: 'IForce', value: 319 }, + { name: 'NBodyForce', value: 10498 }, + { name: 'Particle', value: 2822 }, + { name: 'Simulation', value: 9983 }, + { name: 'Spring', value: 2213 }, + { name: 'SpringForce', value: 1681 } + ] + }, + { + name: 'data', + children: [ + { + name: 'converters', + children: [ + { name: 'Converters', value: 721 }, + { name: 'DelimitedTextConverter', value: 4294 }, + { name: 'GraphMLConverter', value: 9800 }, + { name: 'IDataConverter', value: 1314 }, + { name: 'JSONConverter', value: 2220 } + ] + }, + { name: 'DataField', value: 1759 }, + { name: 'DataSchema', value: 2165 }, + { name: 'DataSet', value: 586 }, + { name: 'DataSource', value: 3331 }, + { name: 'DataTable', value: 772 }, + { name: 'DataUtil', value: 3322 } + ] + }, + { + name: 'vis', + children: [ + { + name: 'controls', + children: [ + { name: 'AnchorControl', value: 2138 }, + { name: 'ClickControl', value: 3824 }, + { name: 'Control', value: 1353 }, + { name: 'ControlList', value: 4665 }, + { name: 'DragControl', value: 2649 }, + { name: 'ExpandControl', value: 2832 }, + { name: 'HoverControl', value: 4896 }, + { name: 'IControl', value: 763 }, + { name: 'PanZoomControl', value: 5222 }, + { name: 'SelectionControl', value: 7862 }, + { name: 'TooltipControl', value: 8435 } + ] + }, + { + name: 'operator', + children: [ + { + name: 'layout', + children: [ + { name: 'AxisLayout', value: 6725 }, + { name: 'BundledEdgeRouter', value: 3727 }, + { name: 'CircleLayout', value: 9317 }, + { name: 'CirclePackingLayout', value: 12003 }, + { name: 'DendrogramLayout', value: 4853 }, + { name: 'ForceDirectedLayout', value: 8411 }, + { name: 'IcicleTreeLayout', value: 4864 }, + { name: 'IndentedTreeLayout', value: 3174 }, + { name: 'Layout', value: 7881 }, + { name: 'NodeLinkTreeLayout', value: 12870 }, + { name: 'PieLayout', value: 2728 }, + { name: 'RadialTreeLayout', value: 12348 }, + { name: 'RandomLayout', value: 870 }, + { name: 'StackedAreaLayout', value: 9121 }, + { name: 'TreeMapLayout', value: 9191 } + ] + }, + { + name: 'encoder', + children: [ + { name: 'ColorEncoder', value: 3179 }, + { name: 'Encoder', value: 4060 }, + { name: 'PropertyEncoder', value: 4138 }, + { name: 'ShapeEncoder', value: 1690 }, + { name: 'valueEncoder', value: 1830 } + ] + }, + { + name: 'distortion', + children: [ + { name: 'BifocalDistortion', value: 4461 }, + { name: 'Distortion', value: 6314 }, + { name: 'FisheyeDistortion', value: 3444 } + ] + }, + { + name: 'filter', + children: [ + { name: 'FisheyeTreeFilter', value: 5219 }, + { name: 'GraphDistanceFilter', value: 3165 }, + { name: 'VisibilityFilter', value: 3509 } + ] + }, + { + name: 'label', + children: [ + { name: 'Labeler', value: 9956 }, + { name: 'RadialLabeler', value: 3899 }, + { name: 'StackedAreaLabeler', value: 3202 } + ] + }, + { name: 'IOperator', value: 1286 }, + { name: 'Operator', value: 2490 }, + { name: 'OperatorList', value: 5248 }, + { name: 'OperatorSequence', value: 4190 }, + { name: 'OperatorSwitch', value: 2581 }, + { name: 'SortOperator', value: 2023 } + ] + }, + { + name: 'data', + children: [ + { + name: 'render', + children: [ + { name: 'ArrowType', value: 698 }, + { name: 'EdgeRenderer', value: 5569 }, + { name: 'IRenderer', value: 353 }, + { name: 'ShapeRenderer', value: 2247 } + ] + }, + { name: 'Data', value: 20544 }, + { name: 'DataList', value: 19788 }, + { name: 'DataSprite', value: 10349 }, + { name: 'EdgeSprite', value: 3301 }, + { name: 'NodeSprite', value: 19382 }, + { name: 'ScaleBinding', value: 11275 }, + { name: 'Tree', value: 7147 }, + { name: 'TreeBuilder', value: 9930 } + ] + }, + { + name: 'axis', + children: [ + { name: 'Axes', value: 1302 }, + { name: 'Axis', value: 24593 }, + { name: 'AxisGridLine', value: 652 }, + { name: 'AxisLabel', value: 636 }, + { name: 'CartesianAxes', value: 6703 } + ] + }, + { + name: 'events', + children: [ + { name: 'DataEvent', value: 2313 }, + { name: 'SelectionEvent', value: 1880 }, + { name: 'TooltipEvent', value: 1701 }, + { name: 'VisualizationEvent', value: 1117 } + ] + }, + { + name: 'legend', + children: [ + { name: 'Legend', value: 20859 }, + { name: 'LegendItem', value: 4614 }, + { name: 'LegendRange', value: 10530 } + ] + }, + { name: 'Visualization', value: 16540 } + ] + }, + { + name: 'display', + children: [ + { name: 'DirtySprite', value: 8833 }, + { name: 'LineSprite', value: 1732 }, + { name: 'RectSprite', value: 3623 }, + { name: 'TextSprite', value: 10066 } + ] + }, + { + name: 'analytics', + children: [ + { + name: 'graph', + children: [ + { name: 'BetweennessCentrality', value: 3534 }, + { name: 'LinkDistance', value: 5731 }, + { name: 'MaxFlowMinCut', value: 7840 }, + { name: 'ShortestPaths', value: 5914 }, + { name: 'SpanningTree', value: 3416 } + ] + }, + { + name: 'cluster', + children: [ + { name: 'AgglomerativeCluster', value: 3938 }, + { name: 'CommunityStructure', value: 3812 }, + { name: 'HierarchicalCluster', value: 6714 }, + { name: 'MergeEdge', value: 743 } + ] + }, + { name: 'optimization', children: [{ name: 'AspectRatioBanker', value: 7074 }] } + ] + }, + { name: 'flex', children: [{ name: 'FlareVis', value: 4116 }] } + ]; + const spec = { + type: 'sunburst', + offsetX: 0, + offsetY: 0, + categoryField: 'name', + valueField: 'value', + outerRadius: 1, + innerRadius: 0, + gap: 5, + drill: true, + labelAutoVisible: { + enable: true, + circumference: 5 + }, + labelLayout: { + align: 'center', + rotate: 'radial' + }, + sunburst: { + visible: true, + style: { + fillOpacity: datum => { + return datum.isLeaf ? 0.4 : 0.8; + } + } + }, + label: { + visible: true, + style: { + fontSize: 12, + fillOpacity: datum => { + return datum.isLeaf ? 0.4 : 0.8; + } + } + }, + tooltip: { + mark: { + title: { + value: val => { + return val?.datum?.map(data => data.name).join(' / '); + } + } + } + }, + data: [ + { + id: 'data', + values: data + } + ], + animationEnter: { + label: { + type: 'fadeIn' + }, + easing: 'cubicInOut', + duration: 1000 + }, + animationExit: { + easing: 'cubicInOut', + duration: 1000 + }, + animationUpdate: { + easing: 'cubicInOut', + duration: 1000 + } + }; + + return spec; + }, + async exercise(page) { + // 源三次 click 以当前图元定位为下钻、回退、另一分支下钻;旧宿主坐标不作为精确目标证据。 + await page.evaluate(() => { + window.__drillEvents = []; + window.__visualChart.on('drill', e => { + window.__drillEvents.push({ type: e.value.type, path: [...e.value.path] }); + }); + }); + const snapshots = []; + for (const index of [0, -1, 1]) { + if (index < 0) await page.mouse.click(790, 590); + else { + const p = await page.evaluate(index => { + const g = window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .filter(g => !g.context?.data?.[0]?.isLeaf && g.attribute.innerRadius === 0)[index]; + if (!g) throw Error('缺少顶层父扇区'); + const a = g.attribute, + m = g.globalTransMatrix, + angle = (a.startAngle + a.endAngle) / 2, + r = (a.innerRadius + a.outerRadius) / 2, + x = r * Math.cos(angle), + y = r * Math.sin(angle); + return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f }; + }, index); + await page.mouse.click(p.x, p.y); + } + await page.mouse.move(950, 750); + await interactionFrame(page); + snapshots.push( + await page.evaluate(() => + JSON.stringify( + window.__visualChart + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => [ + g.attribute.startAngle, + g.attribute.endAngle, + g.attribute.innerRadius, + g.attribute.outerRadius + ]) + ) + ) + ); + } + await page.evaluate(snapshots => { + window.__drillGeometries = snapshots; + }, snapshots); + }, + async verify(page) { + // 路径应下钻、恢复根层、再下钻,三步均须产生实际几何变化。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const e = window.__drillEvents, + g = window.__drillGeometries; + if (e?.length !== 3 || e[0].path.length !== 1 || e[1].path.length !== 0 || e[2].path.length !== 1) + throw Error('下钻/回退路径不正确'); + if (g?.length !== 3 || g[0] === g[1] || g[1] === g[2]) throw Error('下钻没有改变几何布局'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/treemap-label-state.mjs b/packages/vchart/__tests__/visual/cases/interaction/treemap-label-state.mjs new file mode 100644 index 0000000000..6583477ba7 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/treemap-label-state.mjs @@ -0,0 +1,220 @@ +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 65969e5f89f0d500a1fdd478 + * 验证目的:矩形树图标签同步选中状态。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'treemap', + data: [ + { + id: 'data', + values: [ + { + name: 'add', + value: 593 + }, + { + name: 'and', + value: 330 + }, + { + name: 'average', + value: 287 + }, + { + name: 'count', + value: 277 + }, + { + name: 'distinct', + value: 292 + }, + { + name: 'div', + value: 595 + }, + { + name: 'eq', + value: 594 + }, + { + name: 'fn', + value: 460 + }, + { + name: 'gt', + value: 603 + }, + { + name: 'gte', + value: 625 + }, + { + name: 'iff', + value: 748 + }, + { + name: 'isa', + value: 461 + }, + { + name: 'lt', + value: 597 + }, + { + name: 'lte', + value: 619 + }, + { + name: 'max', + value: 283 + }, + { + name: 'min', + value: 283 + }, + { + name: 'mod', + value: 591 + }, + { + name: 'mul', + value: 603 + }, + { + name: 'neq', + value: 599 + }, + { + name: 'not', + value: 386 + }, + { + name: 'or', + value: 323 + }, + { + name: 'orderby', + value: 307 + }, + { + name: 'range', + value: 772 + }, + { + name: 'select', + value: 296 + }, + { + name: 'stddev', + value: 363 + }, + { + name: 'sub', + value: 600 + }, + { + name: 'sum', + value: 280 + }, + { + name: 'update', + value: 307 + }, + { + name: 'variance', + value: 335 + }, + { + name: 'where', + value: 299 + }, + { + name: 'xor', + value: 354 + }, + { + name: '_', + value: 264 + } + ] + } + ], + leaf: { + state: { + selected: { + fill: 'red' + } + } + }, + categoryField: 'name', + valueField: 'value', + label: { + visible: true, + syncState: true, + smartInvert: true, + style: { + fontSize: 12 + }, + state: { + selected: { + fill: 'blue' + } + } + }, + region: [ + { + clip: true, + style: { + cornerRadius: 10 + } + } + ], + tooltip: { + transitionDuration: 0 + }, + select: {} + }; + }, + async exercise(page) { + // 对实际图元执行来源动作,不靠固定等待冒充动作完成。 + const p = await page.evaluate(mark => { + const chart = window.__visualChart; + let graphic; + for (const s of chart.getChart().getAllSeries()) { + const m = s.getMarks().find(m => m.name === mark); + graphic = m?.getGraphics().find(g => g.attribute.visible !== false && g.globalAABBBounds.width() > 0); + if (graphic) break; + } + if (!graphic) throw Error('缺少交互图元 ' + mark); + const a = graphic.attribute, + b = graphic.globalAABBBounds; + if (a.startAngle !== undefined) { + const angle = (a.startAngle + a.endAngle) / 2, + radius = ((a.innerRadius || 0) + a.outerRadius) / 2; + const p = { x: radius * Math.cos(angle), y: radius * Math.sin(angle) }; + return graphic.globalTransMatrix.transformPoint(p, {}); + } + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }, 'leaf'); + await page.mouse.click(p.x, p.y); + }, + async verify(page) { + // 配置、状态及实际过滤结果须满足场景目的;动作未生效必须失败。 + await verifySpec(page, this.createSpec()); + await page.waitForFunction(() => { + const all = window.__visualChart.getStage().findAll(g => g.currentStates?.includes('selected'), true); + return all.length > 0; + }); + await page.waitForFunction( + () => + window.__visualChart.getStage().findAll(g => g.type === 'text' && g.currentStates?.includes('selected'), true) + .length > 0 + ); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/interaction/treemap-selection-persist.mjs b/packages/vchart/__tests__/visual/cases/interaction/treemap-selection-persist.mjs new file mode 100644 index 0000000000..837c84c04d --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/interaction/treemap-selection-persist.mjs @@ -0,0 +1,226 @@ +import { interactionTarget, interactionFrame } from '../../interaction-helpers.mjs'; +import { verifySpec } from '../../helpers.mjs'; +/** + * BugServer case IDs: 6582ac01847639595bfc6181 + * 验证目的:矩形树图 triggerOff=none 时空白点击保留选择。 + * 保留条件:源数据、配置和录制动作类型;按本地图元定位替代旧宿主坐标。 + * 覆盖边界:只验证本文件指定的动作与结果,不宣称重放全部录制子例。 + */ +export default { + createSpec() { + // 每次返回独立的源配置与固定数据。 + return { + type: 'treemap', + data: [ + { + id: 'data', + values: [ + { + name: 'add', + value: 593 + }, + { + name: 'and', + value: 330 + }, + { + name: 'average', + value: 287 + }, + { + name: 'count', + value: 277 + }, + { + name: 'distinct', + value: 292 + }, + { + name: 'div', + value: 595 + }, + { + name: 'eq', + value: 594 + }, + { + name: 'fn', + value: 460 + }, + { + name: 'gt', + value: 603 + }, + { + name: 'gte', + value: 625 + }, + { + name: 'iff', + value: 748 + }, + { + name: 'isa', + value: 461 + }, + { + name: 'lt', + value: 597 + }, + { + name: 'lte', + value: 619 + }, + { + name: 'max', + value: 283 + }, + { + name: 'min', + value: 283 + }, + { + name: 'mod', + value: 591 + }, + { + name: 'mul', + value: 603 + }, + { + name: 'neq', + value: 599 + }, + { + name: 'not', + value: 386 + }, + { + name: 'or', + value: 323 + }, + { + name: 'orderby', + value: 307 + }, + { + name: 'range', + value: 772 + }, + { + name: 'select', + value: 296 + }, + { + name: 'stddev', + value: 363 + }, + { + name: 'sub', + value: 600 + }, + { + name: 'sum', + value: 280 + }, + { + name: 'update', + value: 307 + }, + { + name: 'variance', + value: 335 + }, + { + name: 'where', + value: 299 + }, + { + name: 'xor', + value: 354 + }, + { + name: '_', + value: 264 + } + ] + } + ], + leaf: { + state: { + selected: { + fill: 'red' + } + } + }, + categoryField: 'name', + valueField: 'value', + label: { + visible: true, + style: { + fontSize: 12 + } + }, + tooltip: false, + region: [ + { + clip: true, + style: { + cornerRadius: 10 + } + } + ], + select: { + triggerOff: 'none' + } + }; + }, + async exercise(page) { + // 对两个不同叶节点执行源 click,再以空白点击验证 triggerOff:none 的保留行为。 + const observations = []; + for (const index of [0, 1]) { + const p = await interactionTarget(page, 'leaf', index); + await page.mouse.click(p.x, p.y); + await interactionFrame(page); + observations.push( + await page.evaluate(() => + window.__visualChart + .getChart() + .getAllSeries()[0] + .getMarks() + .find(m => m.name === 'leaf') + .getGraphics() + .flatMap((g, i) => (g.currentStates?.includes('selected') ? [i] : [])) + ) + ); + } + await page.mouse.click(790, 590); + await page.mouse.move(950, 750); + await interactionFrame(page); + await page.evaluate(observations => { + window.__leafSelections = observations; + }, observations); + }, + async verify(page) { + // 第二个叶节点必须被选中,空白点击后仍保留;仅执行一次 click 无法满足两步证据。 + await verifySpec(page, this.createSpec()); + await page.evaluate(() => { + const rows = window.__leafSelections; + if ( + rows?.length !== 2 || + !rows[0].includes(0) || + !rows[1].includes(1) || + JSON.stringify(rows[0]) === JSON.stringify(rows[1]) + ) + throw Error('叶节点选择没有切换'); + const indices = window.__visualChart + .getChart() + .getAllSeries()[0] + .getMarks() + .find(m => m.name === 'leaf') + .getGraphics() + .flatMap((g, i) => (g.currentStates?.includes('selected') ? [i] : [])); + if (JSON.stringify(indices) !== JSON.stringify(rows[1])) throw Error('空白点击清除了持久选择'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/layout/grid-multiple-axes.mjs b/packages/vchart/__tests__/visual/cases/layout/grid-multiple-axes.mjs new file mode 100644 index 0000000000..1bb5212482 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/layout/grid-multiple-axes.mjs @@ -0,0 +1,589 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 646745e9cb5fa8011f4e031e + * 迁移方式:保留原始配置及数据,适配本地测试宿主。 + * 验证目的:三区域网格布局中多折线与独立数值轴。 + * 保留条件:9×9 网格、三区域五系列八轴、三图例、crosshair 配置。 + * 改写说明:仅替换说明中提及的通用类目或标题文本;数值、顺序及配置组合保留。 + * 覆盖边界:只比较源代码提供的静态最终状态;不推断历史缺陷或未提供的交互。 + */ +export default { + createSpec() { + // 每次返回保留来源条件的新配置,避免两侧共享可变对象。 + return { + type: 'common', + stackField: 'stack', + seriesField: 'type', + layout: { + type: 'grid', + col: 9, + row: 9, + colWidth: [ + { + index: 0, + size: 50 + }, + { + index: 4, + size: 50 + }, + { + index: 8, + size: 50 + } + ], + rowHeight: [ + { + index: 0, + size: 50 + }, + { + index: 4, + size: 50 + }, + { + index: 8, + size: 50 + } + ], + elements: [ + { + modelId: 'legend0', + col: 2, + row: 3 + }, + { + modelId: 'legend1', + col: 6, + row: 3 + }, + { + modelId: 'legend2', + col: 2, + row: 7 + }, + { + modelId: 'region0', + col: 2, + row: 1 + }, + { + modelId: 'region1', + col: 6, + row: 1 + }, + { + modelId: 'region2', + col: 2, + row: 5 + }, + { + modelId: 'axesLeft0', + col: 1, + row: 1 + }, + { + modelId: 'axesLeft1', + col: 5, + row: 1 + }, + { + modelId: 'axesRight0', + col: 3, + row: 1 + }, + { + modelId: 'axesRight1', + col: 7, + row: 1 + }, + { + modelId: 'axesRight2', + col: 3, + row: 5 + }, + { + modelId: 'axesBottom0', + col: 2, + row: 2 + }, + { + modelId: 'axesBottom1', + col: 6, + row: 2 + }, + { + modelId: 'axesBottom2', + col: 2, + row: 6 + } + ] + }, + region: [ + { + id: 'region0' + }, + { + id: 'region1' + }, + { + id: 'region2' + } + ], + legends: [ + { + padding: 0, + visible: true, + orient: 'bottom', + id: 'legend0', + position: 'start', + regionIndex: [0], + item: { + visible: true + } + }, + { + padding: 0, + visible: true, + orient: 'bottom', + id: 'legend1', + position: 'start', + regionIndex: [1], + item: { + visible: true + } + }, + { + padding: 0, + visible: true, + orient: 'bottom', + id: 'legend2', + position: 'start', + regionIndex: [2], + item: { + visible: true + } + } + ], + series: [ + { + type: 'line', + data: { + id: 'A', + values: [ + { + x: 0, + y: 13, + type: 'A' + }, + { + x: 1, + y: 42, + type: 'A' + }, + { + x: 2, + y: 159, + type: 'A' + }, + { + x: 3, + y: 135, + type: 'A' + }, + { + x: 4, + y: 66, + type: 'A' + }, + { + x: 5, + y: 43, + type: 'A' + }, + { + x: 6, + y: 280, + type: 'A' + }, + { + x: 7, + y: 200, + type: 'A' + }, + { + x: 8, + y: 16, + type: 'A' + }, + { + x: 9, + y: 109, + type: 'A' + } + ] + }, + regionIndex: 0, + id: 'line0', + xField: 'x', + yField: 'y', + seriesField: 'type' + }, + { + type: 'line', + data: { + id: 'B', + values: [ + { + x: 0, + y: 67, + type: 'B' + }, + { + x: 1, + y: 268, + type: 'B' + }, + { + x: 2, + y: 175, + type: 'B' + }, + { + x: 3, + y: 179, + type: 'B' + }, + { + x: 4, + y: 41, + type: 'B' + }, + { + x: 5, + y: 142, + type: 'B' + }, + { + x: 6, + y: 142, + type: 'B' + }, + { + x: 7, + y: 236, + type: 'B' + }, + { + x: 8, + y: 215, + type: 'B' + }, + { + x: 9, + y: 196, + type: 'B' + } + ] + }, + regionIndex: 0, + id: 'line1', + xField: 'x', + yField: 'y', + seriesField: 'type' + }, + { + type: 'line', + data: { + id: 'C', + values: [ + { + x: 0, + y: 55, + type: 'C' + }, + { + x: 1, + y: 136, + type: 'C' + }, + { + x: 2, + y: 265, + type: 'C' + }, + { + x: 3, + y: 106, + type: 'C' + }, + { + x: 4, + y: 154, + type: 'C' + }, + { + x: 5, + y: 86, + type: 'C' + }, + { + x: 6, + y: 89, + type: 'C' + }, + { + x: 7, + y: 104, + type: 'C' + }, + { + x: 8, + y: 100, + type: 'C' + }, + { + x: 9, + y: 166, + type: 'C' + } + ] + }, + regionIndex: 1, + id: 'line2', + xField: 'x', + yField: 'y', + seriesField: 'type' + }, + { + type: 'line', + data: { + id: 'D', + values: [ + { + x: 0, + y: 124, + type: 'D' + }, + { + x: 1, + y: 242, + type: 'D' + }, + { + x: 2, + y: 182, + type: 'D' + }, + { + x: 3, + y: 179, + type: 'D' + }, + { + x: 4, + y: 143, + type: 'D' + }, + { + x: 5, + y: 291, + type: 'D' + }, + { + x: 6, + y: 248, + type: 'D' + }, + { + x: 7, + y: 90, + type: 'D' + }, + { + x: 8, + y: 74, + type: 'D' + }, + { + x: 9, + y: 291, + type: 'D' + } + ] + }, + regionIndex: 1, + id: 'line3', + xField: 'x', + yField: 'y', + seriesField: 'type' + }, + { + type: 'line', + data: { + id: 'E', + values: [ + { + x: 0, + y: 13, + type: 'E' + }, + { + x: 1, + y: 243, + type: 'E' + }, + { + x: 2, + y: 282, + type: 'E' + }, + { + x: 3, + y: 252, + type: 'E' + }, + { + x: 4, + y: 217, + type: 'E' + }, + { + x: 5, + y: 282, + type: 'E' + }, + { + x: 6, + y: 49, + type: 'E' + }, + { + x: 7, + y: 26, + type: 'E' + }, + { + x: 8, + y: 115, + type: 'E' + }, + { + x: 9, + y: 114, + type: 'E' + } + ] + }, + regionIndex: 2, + id: 'line4', + xField: 'x', + yField: 'y', + seriesField: 'type' + } + ], + crosshair: { + trigger: 'hover', + gridZindex: 600, + xField: { + line: { + visible: true, + type: 'line', + style: { + opacity: 1, + color: 'red', + size: 2 + } + }, + label: { + visible: true + } + }, + yField: { + line: { + visible: true, + type: 'line', + style: { + opacity: 1, + color: 'red', + size: 2 + } + }, + label: { + visible: true + } + } + }, + axes: [ + { + orient: 'left', + regionIndex: 0, + seriesId: 'line0', + id: 'axesLeft0' + }, + { + orient: 'left', + regionIndex: 1, + seriesId: 'line2', + id: 'axesLeft1' + }, + { + orient: 'right', + regionIndex: 0, + seriesId: 'line1', + id: 'axesRight0' + }, + { + orient: 'right', + regionIndex: 1, + seriesId: 'line3', + id: 'axesRight1' + }, + { + orient: 'right', + regionIndex: 2, + seriesId: 'line4', + id: 'axesRight2' + }, + { + orient: 'bottom', + label: { + visible: true + }, + regionIndex: [0], + id: 'axesBottom0' + }, + { + orient: 'bottom', + label: { + visible: true + }, + regionIndex: [1], + id: 'axesBottom1' + }, + { + orient: 'bottom', + title: { + visible: true, + style: { + text: 'Axis 1' + } + }, + regionIndex: [2], + id: 'axesBottom2' + } + ] + }; + }, + async verify(page) { + // 核对完整来源配置及实际绘制;布局差异交由截图比较。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + await page.evaluate(() => { + const chart = window.__visualChart.getChart(); + if (chart.getAllRegions().length !== 3 || chart.getAllSeries().length !== 5) + throw new Error('网格区域或系列丢失'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/layout/media-query-width.mjs b/packages/vchart/__tests__/visual/cases/layout/media-query-width.mjs new file mode 100644 index 0000000000..5db4919cf5 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/layout/media-query-width.mjs @@ -0,0 +1,135 @@ +import { verifyRendered } from '../../helpers.mjs'; +import { interactionFrame } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 66c4659ac3ab4200c6b4fdc5 + * 迁移边界:注册插件后按源 spec 重建实例,再缩窄至 160×160;验证 maxWidth:180 隐藏左轴。宽度恢复时标签未恢复,保留为待调查问题,未宣称覆盖恢复。 + * 验证目的:来源媒体查询注册、缩窄后隐藏左侧坐标轴标签。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const media = [ + { action: [{ filterType: 'dataZoom', spec: { visible: false } }], query: { maxHeight: 300 } }, + { + action: [ + { filterType: 'title', spec: { visible: false } }, + { filter: { orient: 'bottom' }, filterType: 'axes', spec: { title: { visible: false } } }, + { filter: { orient: 'top' }, filterType: 'axes', spec: { title: { visible: false } } } + ], + query: { maxHeight: 180 } + }, + { + action: [ + { filter: { orient: 'top' }, filterType: 'legends', spec: { visible: false } }, + { filter: { orient: 'bottom' }, filterType: 'legends', spec: { visible: false } } + ], + query: { maxHeight: 150 } + }, + { + action: [ + { + filter: { orient: 'top' }, + filterType: 'axes', + spec: { tick: { visible: false }, label: { visible: false } } + }, + { + filter: { orient: 'bottom' }, + filterType: 'axes', + spec: { tick: { visible: false }, label: { visible: false } } + } + ], + query: { maxHeight: 120 } + }, + { + action: [ + { filter: { orient: 'right' }, filterType: 'axes', spec: { title: { visible: false } } }, + { filter: { orient: 'left' }, filterType: 'axes', spec: { title: { visible: false } } }, + { filter: { orient: 'left' }, filterType: 'legends', spec: { visible: false } }, + { filter: { orient: 'right' }, filterType: 'legends', spec: { visible: false } }, + { filterType: 'indicator', spec: { visible: false } } + ], + query: { maxWidth: 200 } + }, + { + action: [ + { + filter: { orient: 'left' }, + filterType: 'axes', + spec: { tick: { visible: false }, label: { visible: false } } + }, + { + filter: { orient: 'right' }, + filterType: 'axes', + spec: { tick: { visible: false }, label: { visible: false } } + } + ], + query: { maxWidth: 180 } + } + ]; + const spec = { + type: 'bar', + height: 160, + media, + stack: true, + label: { visible: true, position: 'inside' }, + data: [ + { + id: 'barData', + values: [ + { State: 'AL', age: 'Under 5 Years', population: 100, type: 'a' }, + { State: 'AL', age: '5 to 13 Years', population: 20, type: 'a' } + ] + } + ], + xField: 'State', + yField: 'population', + seriesField: 'age', + color: ['red', 'blue'], + axes: [{ orient: 'left' }] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const { cases, loadCase } = await import('./cases/index.mjs'); + const item = await loadCase(cases.find(c => c.id === 'media-query-width')); + window.VChart.registerMediaQuery(); + window.__visualChart.release(); + const c = (window.__visualChart = new window.VChart.default( + { ...item.createSpec(), animation: false }, + { + dom: 'chart', + animation: false, + autoFit: false, + theme: { fontFamily: 'Arial', tooltip: { transitionDuration: 0 } }, + onError: e => window.__visualErrors.push(String(e)) + } + )); + await c.renderAsync(); + window.__mediaWide = c + .getStage() + .findAll(g => g.name === 'axis-label' && g.attribute.visible !== false, true).length; + await c.resize(160, 160); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + count = c.getStage().findAll(g => g.name === 'axis-label' && g.attribute.visible !== false, true).length; + if ( + window.__mediaWide === undefined || + count >= window.__mediaWide || + c.getSpec().axes.find(a => a.orient === 'left').label.visible !== false + ) + throw Error('媒体查询未随缩窄隐藏左轴标签'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/theme/theme-chart-components.mjs b/packages/vchart/__tests__/visual/cases/theme/theme-chart-components.mjs new file mode 100644 index 0000000000..e50dfd3ed3 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/theme/theme-chart-components.mjs @@ -0,0 +1,79 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionFrame } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 6704f50622f17300a767a2c0 + * 迁移边界:保留来源先注册并激活主题、再创建实例的顺序;不以实例后置切换替代标签初始化。 + * 验证目的:来源图表主题启用柱标签并定位左侧图例。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'bar', + data: { + values: [ + { time: '2:00', value: 8 }, + { time: '4:00', value: 9 }, + { time: '6:00', value: 11 }, + { time: '8:00', value: 14 }, + { time: '10:00', value: 16 }, + { time: '12:00', value: 17 }, + { time: '14:00', value: 17 }, + { time: '16:00', value: 16 }, + { time: '18:00', value: 15 } + ] + }, + legends: {}, + xField: 'time', + yField: 'value' + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const myTheme = { + series: { bar: { label: { visible: true } } }, + chart: { bar: { component: { discreteLegend: { orient: 'left', position: 'end' } } } } + }; + window.VChart.ThemeManager.registerTheme('user', myTheme); + window.VChart.ThemeManager.setCurrentTheme('user'); + const { cases, loadCase } = await import('./cases/index.mjs'); + const spec = (await loadCase(cases.find(c => c.id === 'theme-chart-components'))).createSpec(); + window.__visualChart.release(); + window.__visualChart = new window.VChart.default( + { ...spec, animation: false }, + { + dom: 'chart', + animation: false, + autoFit: false, + theme: { fontFamily: 'Arial', tooltip: { transitionDuration: 0 } }, + onError: e => window.__visualErrors.push(String(e)) + } + ); + await window.__visualChart.renderAsync(); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const c = window.__visualChart, + legend = c + .getChart() + .getAllComponents() + .find(c => c.type === 'discreteLegend'); + if (legend?.getSpec().orient !== 'left') throw Error('图例未使用图表主题'); + const texts = c + .getStage() + .findAll(g => g.name === 'data-label', true) + .flatMap(g => g.findAll(x => x.type === 'text' && x.attribute.visible !== false, true)); + if (!texts.length) throw Error('主题未开启柱标签'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/theme/theme-runtime-switch.mjs b/packages/vchart/__tests__/visual/cases/theme/theme-runtime-switch.mjs new file mode 100644 index 0000000000..24a568bf34 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/theme/theme-runtime-switch.mjs @@ -0,0 +1,638 @@ +import { verifySourceSpec, verifyRendered } from '../../helpers.mjs'; +import { interactionFrame } from '../../interaction-helpers.mjs'; +/** + * BugServer case IDs: 6570529d8241e88116f3cdfa + * 验证目的:来源主题注册和实例切换后的圆角与配色。 + * 保留条件:来源数据、顺序、配置和回调;仅适配宿主。 + * 交互说明:按实际图元定位重建来源动作,不宣称重放旧宿主坐标。 + */ +export default { + createSpec() { + // 每次生成独立的完整来源 spec。 + const spec = { + type: 'common', + data: [ + { + id: 'id0', + values: [ + { time: '2:00', value: 8, type: 'Douyin' }, + { time: '4:00', value: 9, type: 'Douyin' }, + { time: '6:00', value: 11, type: 'Douyin' }, + { time: '8:00', value: 14, type: 'Douyin' }, + { time: '10:00', value: 16, type: 'Douyin' }, + { time: '12:00', value: 17, type: 'Douyin' }, + { time: '14:00', value: 17, type: 'Douyin' }, + { time: '16:00', value: 16, type: 'Douyin' }, + { time: '18:00', value: 15, type: 'Douyin' }, + { time: '2:00', value: 7, type: 'Bilibili' }, + { time: '4:00', value: 8, type: 'Bilibili' }, + { time: '6:00', value: 9, type: 'Bilibili' }, + { time: '8:00', value: 10, type: 'Bilibili' }, + { time: '10:00', value: 9, type: 'Bilibili' }, + { time: '12:00', value: 12, type: 'Bilibili' }, + { time: '14:00', value: 14, type: 'Bilibili' }, + { time: '16:00', value: 12, type: 'Bilibili' }, + { time: '18:00', value: 14, type: 'Bilibili' } + ] + }, + { + id: 'id1', + values: [ + { time: '2:00', value: 15, type: 'Total' }, + { time: '4:00', value: 17, type: 'Total' }, + { time: '6:00', value: 20, type: 'Total' }, + { time: '8:00', value: 24, type: 'Total' }, + { time: '10:00', value: 25, type: 'Total' }, + { time: '12:00', value: 29, type: 'Total' }, + { time: '14:00', value: 31, type: 'Total' }, + { time: '16:00', value: 28, type: 'Total' }, + { time: '18:00', value: 29, type: 'Total' } + ] + } + ], + series: [ + { + type: 'bar', + dataIndex: 0, + label: { visible: true }, + xField: ['time', 'type'], + yField: 'value', + seriesField: 'type' + } + ], + legends: { visible: true, orient: 'right' }, + axes: [ + { orient: 'bottom', type: 'band' }, + { orient: 'left', type: 'linear' } + ] + }; + return spec; + }, + async exercise(page) { + // 执行来源配置或回调要求的动作并保存实际结果。 + + await page.evaluate(async () => { + const chartSpace = window.__visualChart; + window.__themeBefore = chartSpace + .getChart() + .getAllSeries()[0] + .getSeriesMark() + .getGraphics() + .map(g => g.attribute.fill); + const fontFamily = + 'PingFang SC,Microsoft Yahei,system-ui,-apple-system,segoe ui,Roboto,Helvetica,Arial,sans-serif, apple color emoji,segoe ui emoji,segoe ui symbol'; + const getNewTheme = () => { + const series = { + scatter: { + size: 10, + shape: 'circle', + fillOpacity: 1, + strokeOpacity: 1, + label: { + visible: false, + offset: 5, + position: 'top', + style: { lineWidth: 2, stroke: 'white', fontFamily: fontFamily, fontSize: 11 } + } + }, + line: { + size: 10, + fillOpacity: 1, + strokeOpacity: 1, + label: { + visible: false, + position: 'top', + offset: 5, + style: { lineWidth: 2, stroke: 'white', fontFamily: fontFamily, fontSize: 11 } + } + }, + area: { + size: 10, + fillOpacity: 1, + strokeOpacity: 1, + area: { style: { fillOpacity: 0.2 } }, + label: { + visible: false, + offset: 5, + position: 'top', + style: { stroke: 'white', lineWidth: 2, fontFamily: fontFamily, fontSize: 11 } + } + }, + bar: { + bar: { style: { cornerRadius: 10 } }, + fillOpacity: 1, + strokeOpacity: 1, + label: { + visible: false, + position: 'outside', + offset: 5, + style: { lineWidth: 2, stroke: 'red', fontFamily: fontFamily, fontSize: 11 } + } + }, + bar3d: { + fillOpacity: 1, + strokeOpacity: 1, + bar3d: { style: { length: 3 } }, + label: { visible: false, style: { fontFamily: fontFamily, fontSize: 11, offset: 12, position: 'outside' } } + }, + pie: { + fillOpacity: 1, + outerRadius: 0.6, + label: { visible: false, position: 'outside', style: { fontFamily: fontFamily, fontSize: 11 } } + }, + pie3d: { + fillOpacity: 1, + outerRadius: 0.6, + pie3d: { style: { height: 10 } }, + label: { visible: false, position: 'outside', style: { fontFamily: fontFamily, fontSize: 11 } } + }, + map: { + defaultFillColor: '#f3f3f3', + area: { style: { lineWidth: 0.5, strokeOpacity: 1, stroke: 'black' } }, + label: { + interactive: false, + style: { fontFamily: fontFamily, fontSize: 11, textBaseline: 'middle', fill: 'black', stroke: 'white' } + } + }, + radar: { + size: 10, + fillOpacity: 1, + strokeOpacity: 1, + area: { style: { fillOpacity: 0.2 } }, + label: { + visible: false, + offset: 5, + style: { lineWidth: 2, stroke: 'white', fontFamily: fontFamily, fontSize: 11 } + } + }, + dot: { + size: 10, + fillOpacity: 1, + strokeOpacity: 1, + title: { style: { textAlign: 'left', textBaseline: 'middle', fontSize: 10 } }, + subTitle: { style: { textAlign: 'left', textBaseline: 'top', fontSize: 10 } } + }, + link: { size: 10, fillOpacity: 1, strokeOpacity: 1 }, + wordCloud: { word: { padding: 1, style: { textAlign: 'center', textBaseline: 'alphabetic' } } }, + wordCloud3d: { word3d: { padding: 1, style: { textAlign: 'center', textBaseline: 'alphabetic' } } }, + funnel: { + transform: { style: { fill: '#f5f5f5' } }, + label: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: 'white', textBaseline: 'middle', lineWidth: 2 } + }, + outerLabel: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: '#707070' }, + line: { style: { stroke: '#ddd' } } + }, + transformLabel: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: '#707070', textBaseline: 'middle' } + } + }, + funnel3d: { + transform: { style: { fill: '#f5f5f5' } }, + label: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: 'white', textBaseline: 'middle', lineWidth: 2 } + }, + outerLabel: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: '#707070' }, + line: { style: { stroke: '#ddd' } } + }, + transformLabel: { + style: { fontFamily: 'sans-serif', fontSize: 11, fill: '#707070', textBaseline: 'middle' } + } + }, + linearProgress: { bandWidth: 30, track: { style: { fill: '#E7EBED', fillOpacity: 1 } } }, + circularProgress: { outerRadius: 0.8, innerRadius: 0.6 }, + waterfall: { + seriesFieldName: { total: 'total', increase: 'increase', decrease: 'decrease' }, + fillOpacity: 1, + strokeOpacity: 1, + leaderLine: { style: { stroke: 'black', lineWidth: 1, lineDash: [4, 4] } }, + stackLabel: { + offset: 12, + position: 'withChange', + style: { fill: 'black', fontFamily: fontFamily, fontSize: 11 } + }, + label: { visible: false, offset: 12, position: 'inside', style: { fontFamily: fontFamily, fontSize: 11 } } + }, + gauge: { outerRadius: 0.8, innerRadius: 0.6, padAngle: 1.146 }, + gaugePointer: { + pointer: { + type: 'path', + width: 0.4, + height: 0.4, + style: { + path: 'M-0.020059 -0.978425 C-0.018029 -0.9888053 -0.013378 -1 0 -1 C0.01342 -1 0.01812 -0.989146 0.0201 -0.978425 C0.02161 -0.9702819 0.0692 -0.459505 0.09486 -0.184807 C0.10298 -0.097849 0.1089 -0.034548 0.11047 -0.018339 C0.11698 0.04908 0.07373 0.11111 0.00002 0.11111 C-0.07369 0.11111 -0.117184 0.04991 -0.110423 -0.018339 C-0.103662 -0.086591 -0.022089 -0.9680447 -0.020059 -0.978425Z' + } + }, + pin: { + width: 0.025, + height: 0.025, + style: { + path: 'M1 0 C1 0.55228 0.55228 1 0 1 C-0.552285 1 -1 0.55228 -1 0 C-1 -0.552285 -0.552285 -1 0 -1 C0.55228 -1 1 -0.552285 1 0Z', + fill: '#888' + } + }, + pinBackground: { + width: 0.06, + height: 0.06, + style: { + path: 'M1 0 C1 0.55228 0.55228 1 0 1 C-0.552285 1 -1 0.55228 -1 0 C-1 -0.552285 -0.552285 -1 0 -1 C0.55228 -1 1 -0.552285 1 0Z', + fill: '#ddd' + } + } + }, + treemap: { + fillOpacity: 1, + strokeOpacity: 1, + gapWidth: 1, + nodePadding: [5], + nonLeaf: { visible: false, style: { fillOpacity: 0.5 } }, + label: { + style: { + fontFamily: fontFamily, + fontSize: 11, + fill: 'white', + textBaseline: 'middle', + textAlign: 'center' + } + }, + nonLeafLabel: { + padding: 22, + style: { + fill: 'black', + stroke: 'white', + lineWidth: 2, + fontFamily: fontFamily, + fontSize: 11, + textBaseline: 'middle', + textAlign: 'center' + } + } + }, + sunburst: { + innerRadius: 0, + outerRadius: 1, + startAngle: -90, + endAngle: 270, + gap: 0, + labelLayout: { align: 'center', offset: 0, rotate: 'radial' }, + sunburst: { style: { stroke: '#ffffff', fillOpacity: 1, cursor: 'pointer' } }, + label: { visible: true, style: { cursor: 'pointer', fill: '#000000' } } + }, + rangeColumn: { + fillOpacity: 1, + strokeOpacity: 1, + label: { + visible: false, + offset: 5, + position: 'inside', + style: { fill: '#ffffff', fontFamily: fontFamily, fontSize: 11 }, + minLabel: { position: 'end' }, + maxLabel: { position: 'start' } + } + }, + circlePacking: { + layoutPadding: 5, + circlePacking: { visible: true, style: { cursor: 'pointer', stroke: '#ffffff' } }, + label: { visible: true, style: { cursor: 'pointer', fill: '#000000' } } + }, + heatmap: { + fillOpacity: 1, + strokeOpacity: 1, + cell: { style: { shape: 'square' } }, + cellBackground: { visible: false } + } + }; + const component = { + discreteLegend: { + title: { + visible: false, + padding: 0, + textStyle: { fontSize: 12, fill: '#333333', fontWeight: 'normal' }, + space: 12 + }, + item: { + visible: true, + spaceCol: 10, + spaceRow: 10, + padding: { top: 2, bottom: 2, left: 2, right: 2 }, + background: { + state: { + selectedHover: { fill: 'gray', fillOpacity: 0.7 }, + unSelectedHover: { fill: 'gray', fillOpacity: 0.2 } + } + }, + shape: { space: 4, state: { unSelected: { fillOpacity: 0.5 } } }, + label: { space: 4, style: { fill: 'black', fontSize: 12 }, state: { unSelected: { fillOpacity: 0.5 } } } + }, + allowAllCanceled: false + }, + colorLegend: { + horizontal: { + title: { + visible: false, + padding: 0, + textStyle: { fontSize: 12, fontWeight: 'normal', fill: 'rgba(46, 47, 50, 1)' }, + space: 12 + }, + handler: { visible: true }, + rail: { width: 200, height: 8, style: { fill: 'rgba(0,0,0,0.04)' } } + }, + vertical: { + title: { + visible: false, + padding: 0, + textStyle: { fontSize: 12, fontWeight: 'normal', fill: 'rgba(46, 47, 50, 1)' }, + space: 12 + }, + handler: { visible: true }, + rail: { width: 8, height: 200, style: { fill: 'rgba(0,0,0,0.04)' } } + } + }, + sizeLegend: { + horizontal: { + sizeBackground: { fill: 'rgb(205, 205, 205)' }, + track: { style: { fill: 'rgba(20,20,20,0.1)' } }, + rail: { width: 200, height: 4, style: { fill: 'rgba(0,0,0,0.04)' } }, + title: { + visible: false, + padding: 0, + textStyle: { fontSize: 12, fontWeight: 'normal', fill: 'rgba(46, 47, 50, 1)' }, + space: 12 + }, + handler: { visible: true } + }, + vertical: { + sizeBackground: { fill: 'rgb(205, 205, 205)' }, + track: { style: { fill: 'rgba(20,20,20,0.1)' } }, + rail: { width: 4, height: 200, style: { fill: 'rgba(0,0,0,0.04)' } }, + title: { + visible: false, + padding: 0, + textStyle: { fontSize: 12, fontWeight: 'normal', fill: 'rgba(46, 47, 50, 1)' }, + space: 12 + }, + handler: { visible: true } + } + }, + axis: { + domainLine: { visible: true, style: { lineWidth: 1, stroke: '#dfdfdf', strokeOpacity: 1 } }, + grid: { visible: true, style: { lineWidth: 1, stroke: '#dfdfdf', strokeOpacity: 1, lineDash: [4, 4] } }, + subGrid: { visible: false, style: { lineWidth: 1, stroke: '#dfdfdf', strokeOpacity: 1, lineDash: [4, 4] } }, + tick: { visible: true, tickSize: 4, style: { lineWidth: 1, stroke: '#D8DCE3', strokeOpacity: 1 } }, + subTick: { visible: false, tickSize: 2, style: { lineWidth: 1, stroke: '#D8DCE3', strokeOpacity: 1 } }, + label: { + visible: true, + space: 4, + style: { fontSize: 12, fill: '#6F6F6F', fontWeight: 'normal', fillOpacity: 1 } + }, + title: { + space: 4, + autoRotate: true, + style: { fontSize: 12, fill: '#333333', fontWeight: 'normal', fillOpacity: 1 } + } + }, + axisX: { grid: { visible: false } }, + axisY: { domainLine: { visible: true }, tick: { visible: true }, label: { space: 4 } }, + markLine: { + line: { style: { lineDash: [3, 3], stroke: 'rgba(46, 47, 50)' } }, + startSymbol: { + visible: false, + symbolType: 'triangle', + style: { stroke: null, lineWidth: 0, fill: 'rgba(46, 47, 50)', size: 10 } + }, + endSymbol: { + visible: true, + symbolType: 'triangle', + style: { stroke: null, lineWidth: 0, fill: 'rgba(46, 47, 50)', size: 10 } + }, + label: { + refY: 5, + style: { + fontSize: 14, + fontWeight: 'normal', + fontStyle: 'normal', + fill: '#fff', + stroke: '#fff', + lineWidth: 0 + }, + labelBackground: { + padding: { top: 2, bottom: 2, right: 4, left: 4 }, + style: { cornerRadius: 0, fill: 'rgb(48, 115, 242)' } + } + } + }, + markArea: { + area: { style: { fill: 'rgba(46, 47, 50, 0.1)' } }, + label: { + style: { + fontSize: 14, + fontWeight: 'normal', + fontStyle: 'normal', + fill: '#fff', + stroke: '#fff', + lineWidth: 0 + }, + labelBackground: { + padding: { top: 2, bottom: 2, right: 4, left: 4 }, + style: { cornerRadius: 0, fill: '#F68484' } + } + } + }, + markPoint: { + itemLine: { + decorativeLine: { visible: false }, + startSymbol: { size: 5, visible: true, style: { fill: '#eee', stroke: '#eee' } }, + line: { style: { stroke: '#eee' } } + }, + itemContent: { offsetY: -50 } + }, + tooltip: { + panel: { + padding: { top: 10, left: 10, right: 10, bottom: 10 }, + backgroundColor: '#fff', + border: { color: '#ffffff', width: 0, radius: 3 }, + shadow: { x: 0, y: 4, blur: 12, spread: 0, color: 'rgba(0, 0, 0, 0.1)' } + }, + spaceRow: 6, + titleLabel: { + fontFamily: fontFamily, + fontSize: 14, + fontColor: '#4E5969', + textAlign: 'left', + lineHeight: 18, + textBaseline: 'middle', + spacing: 0 + }, + shape: { size: 8, spacing: 6 }, + keyLabel: { + fontFamily: fontFamily, + fontSize: 12, + fontColor: '#4E5969', + textAlign: 'left', + lineHeight: 18, + textBaseline: 'middle', + spacing: 26 + }, + valueLabel: { + fontFamily: fontFamily, + fontSize: 12, + fontColor: '#4E5969', + lineHeight: 18, + textBaseline: 'middle', + spacing: 0 + } + }, + dataZoom: { + showDetail: 'auto', + middleHandler: { + visible: false, + background: { size: 5, style: { stroke: '#D1D5DA', cornerRadius: 2 } }, + icon: { + style: { + size: 8, + fill: 'white', + stroke: '#D1D5DA', + symbolType: + 'M 0.3 -0.5 C 0.41 -0.5 0.5 -0.41 0.5 -0.3 C 0.5 -0.3 0.5 0.3 0.5 0.3 C 0.5 0.41 0.41 0.5 0.3 0.5 C 0.3 0.5 -0.3 0.5 -0.3 0.5 C -0.41 0.5 -0.5 0.41 -0.5 0.3 C -0.5 0.3 -0.5 -0.3 -0.5 -0.3 C -0.5 -0.41 -0.41 -0.5 -0.3 -0.5 C -0.3 -0.5 0.3 -0.5 0.3 -0.5 Z', + lineWidth: 0.5 + } + } + }, + background: { size: 20, style: { fill: '#F6F8FA', stroke: '#F6F8FA', lineWidth: 1 } }, + startHandler: { + style: { + symbolType: + 'M-651.40493822 1451.33576377m0-418.93088554l0-2094.65442779q0-418.93088556 418.93088555-418.93088733l418.93088556 0q418.93088556 0 418.93088553 418.93088733l0 2094.65442779q0 418.93088556-418.93088553 418.93088554l-418.93088556 0q-418.93088556 0-418.93088555-418.93088554Z M-546.67221684 1032.40487819a314.19816417 314.19816417 0 0 0 314.19816418 314.19816421l418.93088555 1e-8a314.19816417 314.19816417 0 0 0 314.19816418-314.19816418l-1e-8-2094.65442779a314.19816417 314.19816417 0 0 0-314.19816417-314.19816596l-418.93088556 0a314.19816417 314.19816417 0 0 0-314.19816417 314.19816596l0 2094.65442775m-104.73272138 4e-8l0-2094.65442779a418.93088556 418.93088556 0 0 1 418.93088555-418.93088733l418.93088556 0a418.93088556 418.93088556 0 0 1 418.93088553 418.93088733l0 2094.65442779a418.93088556 418.93088556 0 0 1-418.93088553 418.93088554l-418.93088556 0a418.93088556 418.93088556 0 0 1-418.93088555-418.93088554z M-232.47405266 404.00854987l-1e-8-837.86177109 104.73272138 0 0 837.86177109z M81.72411149 404.00854987l0-837.86177109 104.7327214 0 0 837.8617711z', + fill: '#FFF', + stroke: '#AEB8C6', + lineWidth: 0.8 + } + }, + endHandler: { + style: { + symbolType: + 'M-651.40493822 1451.33576377m0-418.93088554l0-2094.65442779q0-418.93088556 418.93088555-418.93088733l418.93088556 0q418.93088556 0 418.93088553 418.93088733l0 2094.65442779q0 418.93088556-418.93088553 418.93088554l-418.93088556 0q-418.93088556 0-418.93088555-418.93088554Z M-546.67221684 1032.40487819a314.19816417 314.19816417 0 0 0 314.19816418 314.19816421l418.93088555 1e-8a314.19816417 314.19816417 0 0 0 314.19816418-314.19816418l-1e-8-2094.65442779a314.19816417 314.19816417 0 0 0-314.19816417-314.19816596l-418.93088556 0a314.19816417 314.19816417 0 0 0-314.19816417 314.19816596l0 2094.65442775m-104.73272138 4e-8l0-2094.65442779a418.93088556 418.93088556 0 0 1 418.93088555-418.93088733l418.93088556 0a418.93088556 418.93088556 0 0 1 418.93088553 418.93088733l0 2094.65442779a418.93088556 418.93088556 0 0 1-418.93088553 418.93088554l-418.93088556 0a418.93088556 418.93088556 0 0 1-418.93088555-418.93088554z M-232.47405266 404.00854987l-1e-8-837.86177109 104.73272138 0 0 837.86177109z M81.72411149 404.00854987l0-837.86177109 104.7327214 0 0 837.8617711z', + fill: '#FFF', + stroke: '#AEB8C6', + lineWidth: 0.8 + } + }, + startText: { padding: 8, style: { fontSize: 10, fill: '#6F6F6F' } }, + endText: { padding: 8, style: { fontSize: 10, fill: '#6F6F6F' } }, + selectedBackground: { style: { fill: '#E1E4E8', fillOpacity: 0.5 } }, + dragMask: { style: { fill: '#E1E4E8', fillOpacity: 0.2 } }, + backgroundChart: { + area: { style: { lineWidth: 1, fill: '#D1D5DA' } }, + line: { style: { stroke: '#D1D5DA', lineWidth: 1 } } + }, + selectedBackgroundChart: { + area: { style: { lineWidth: 1, fill: '#D1D5DA' } }, + line: { style: { stroke: '#D1D5DA', lineWidth: 1 } } + } + }, + crosshair: { + xField: { + visible: false, + line: { type: 'rect', visible: true, style: { fill: '#b2bacf', opacity: 0.2, lineDash: [] } }, + label: { + visible: true, + style: { fontFamily: fontFamily, fontWeight: 'normal', fill: '#fff', fontSize: 12 }, + labelBackground: { + padding: { bottom: 2, top: 2, left: 4, right: 4 }, + style: { fill: 'rgba(47, 59, 82, 0.9)', cornerRadius: 4 } + } + } + }, + yField: { + visible: false, + line: { type: 'line', visible: true, style: { stroke: '#b2bacf', opacity: 0.2, lineDash: [] } }, + label: { + visible: true, + style: { fontFamily: fontFamily, fontWeight: 'normal', fill: '#fff', fontSize: 12 }, + labelBackground: { + padding: { bottom: 2, top: 2, left: 4, right: 4 }, + style: { fill: 'rgba(47, 59, 82, 0.9)', cornerRadius: 4 } + } + } + }, + categoryField: { + visible: false, + line: { type: 'line', visible: true, style: { stroke: '#b2bacf', opacity: 0.2, lineDash: [] } }, + label: { + visible: true, + style: { fontFamily: fontFamily, fontWeight: 'normal', fill: '#fff', fontSize: 12 }, + labelBackground: { + padding: { bottom: 2, top: 2, left: 4, right: 4 }, + style: { fill: 'rgba(47, 59, 82, 0.9)', cornerRadius: 4 } + } + } + }, + valueField: { + visible: false, + line: { type: 'line', visible: true, style: { stroke: '#b2bacf', opacity: 0.2, lineDash: [] } }, + label: { + visible: true, + style: { fontFamily: fontFamily, fontWeight: 'normal', fill: '#fff', fontSize: 12 }, + labelBackground: { + padding: { bottom: 2, top: 2, left: 4, right: 4 }, + style: { fill: 'rgba(47, 59, 82, 0.9)', cornerRadius: 4 } + } + } + } + }, + player: { + visible: true, + position: 'start', + slider: { space: 10, trackStyle: { fill: '#63B5FC' }, railStyle: {}, handlerStyle: { stroke: '#63B5FC' } }, + controller: { + start: { order: 0, space: 0, style: { size: 20, fill: '#63B5FC' } }, + pause: { order: 0, space: 0, style: { size: 20, fill: '#63B5FC' } }, + backward: { order: 0, space: 10, position: 'start', style: { size: 20, fill: '#63B5FC' } }, + forward: { order: 0, space: 10, position: 'end', style: { size: 20, fill: '#63B5FC' } } + } + } + }; + const theme = { + name: 'A', + padding: 12, + colorScheme: { + default: [ + '#6690F2', + '#70D6A3', + '#B4E6E2', + '#63B5FC', + '#FF8F62', + '#FFDC83', + '#BCC5FD', + '#A29BFE', + '#63C4C7', + '#F68484' + ] + }, + component, + series + }; + return theme; + }; + + window.VChart.ThemeManager.registerTheme('user', getNewTheme()); + await chartSpace.setCurrentTheme('user'); + }); + await interactionFrame(page); + }, + async verify(page) { + // 核对实际结果;未执行或无效动作必须失败。 + await verifySourceSpec(page); + await verifyRendered(page); + await page.evaluate(() => { + const graphics = window.__visualChart.getChart().getAllSeries()[0].getSeriesMark().getGraphics(); + if ( + !graphics.some(g => g.attribute.cornerRadius === 10) || + JSON.stringify(graphics.map(g => g.attribute.fill)) === JSON.stringify(window.__themeBefore) + ) + throw Error('来源主题未改变圆角与颜色'); + }); + } +}; diff --git a/packages/vchart/__tests__/visual/cases/theme/theme-stack.mjs b/packages/vchart/__tests__/visual/cases/theme/theme-stack.mjs new file mode 100644 index 0000000000..912558fc18 --- /dev/null +++ b/packages/vchart/__tests__/visual/cases/theme/theme-stack.mjs @@ -0,0 +1,260 @@ +import { verifySpec, verifyRendered } from '../../helpers.mjs'; + +/** + * BugServer case IDs: 65f112827c3a0c00c99f2da7 + * 验证目的:堆叠面积图的专用主题样式。 + * 保留条件:title, xField, yField, seriesField, legends, crosshair, axes, theme;原始数值、数据顺序和配置组合。 + * 迁移说明:替换测试宿主。 + * 覆盖边界:只验证保存源码的静态最终状态,不宣称交互或历史缺陷已复现。 + */ +export default { + createSpec() { + // 返回独立配置,保留来源数据及相互关联的设置。 + return { + type: 'area', + data: { + values: [ + { + type: 'Nail polish', + country: 'Africa', + value: 4229 + }, + { + type: 'Nail polish', + country: 'EU', + value: 4376 + }, + { + type: 'Nail polish', + country: 'China', + value: 3054 + }, + { + type: 'Nail polish', + country: 'USA', + value: 12814 + }, + { + type: 'Eyebrow pencil', + country: 'Africa', + value: 3932 + }, + { + type: 'Eyebrow pencil', + country: 'EU', + value: 3987 + }, + { + type: 'Eyebrow pencil', + country: 'China', + value: 5067 + }, + { + type: 'Eyebrow pencil', + country: 'USA', + value: 13012 + }, + { + type: 'Rouge', + country: 'Africa', + value: 5221 + }, + { + type: 'Rouge', + country: 'EU', + value: 3574 + }, + { + type: 'Rouge', + country: 'China', + value: 7004 + }, + { + type: 'Rouge', + country: 'USA', + value: 11624 + }, + { + type: 'Lipstick', + country: 'Africa', + value: 9256 + }, + { + type: 'Lipstick', + country: 'EU', + value: 4376 + }, + { + type: 'Lipstick', + country: 'China', + value: 9054 + }, + { + type: 'Lipstick', + country: 'USA', + value: 8814 + }, + { + type: 'Eyeshadows', + country: 'Africa', + value: 3308 + }, + { + type: 'Eyeshadows', + country: 'EU', + value: 4572 + }, + { + type: 'Eyeshadows', + country: 'China', + value: 12043 + }, + { + type: 'Eyeshadows', + country: 'USA', + value: 12998 + }, + { + type: 'Eyeliner', + country: 'Africa', + value: 5432 + }, + { + type: 'Eyeliner', + country: 'EU', + value: 3417 + }, + { + type: 'Eyeliner', + country: 'China', + value: 15067 + }, + { + type: 'Eyeliner', + country: 'USA', + value: 12321 + }, + { + type: 'Foundation', + country: 'Africa', + value: 13701 + }, + { + type: 'Foundation', + country: 'EU', + value: 5231 + }, + { + type: 'Foundation', + country: 'China', + value: 10119 + }, + { + type: 'Foundation', + country: 'USA', + value: 10342 + }, + { + type: 'Lip gloss', + country: 'Africa', + value: 4008 + }, + { + type: 'Lip gloss', + country: 'EU', + value: 4572 + }, + { + type: 'Lip gloss', + country: 'China', + value: 12043 + }, + { + type: 'Lip gloss', + country: 'USA', + value: 22998 + }, + { + type: 'Mascara', + country: 'Africa', + value: 18712 + }, + { + type: 'Mascara', + country: 'EU', + value: 6134 + }, + { + type: 'Mascara', + country: 'China', + value: 10419 + }, + { + type: 'Mascara', + country: 'USA', + value: 11261 + } + ] + }, + title: { + visible: true, + text: 'Stacked area chart of cosmetic products sales' + }, + xField: 'type', + yField: 'value', + seriesField: 'country', + legends: [ + { + visible: true, + position: 'middle', + orient: 'bottom' + } + ], + crosshair: { + xField: { + visible: true, + label: { + visible: true + } + }, + yField: { + visible: true, + label: { + visible: true + } + } + }, + axes: [ + { + orient: 'bottom', + type: 'band' + }, + { + orient: 'left', + type: 'linear' + }, + { + orient: 'right', + type: 'linear' + } + ], + theme: { + series: { + area_stack: { + area: { + style: { + fill: 'red' + } + }, + seriesMark: 'line' + } + } + } + }; + }, + async verify(page) { + // verifySpec 递归检查全部显式配置和数据;verifyRendered 检查每个系列实际有效几何。 + await verifySpec(page, this.createSpec()); + await verifyRendered(page, 'series'); + } +}; diff --git a/packages/vchart/__tests__/visual/helpers.mjs b/packages/vchart/__tests__/visual/helpers.mjs new file mode 100644 index 0000000000..2a5ba02c48 --- /dev/null +++ b/packages/vchart/__tests__/visual/helpers.mjs @@ -0,0 +1,159 @@ +/** 创建固定的分组柱图数据,避免随机数据引入截图噪声。 */ +export function barSpec() { + return { + type: 'bar', + data: { + id: 'data', + values: [ + { x: 'A', y: 30, group: 'Alpha' }, + { x: 'A', y: 15, group: 'Beta' }, + { x: 'B', y: -12, group: 'Alpha' }, + { x: 'B', y: -22, group: 'Beta' }, + { x: 'C', y: 40, group: 'Alpha' }, + { x: 'C', y: 25, group: 'Beta' } + ] + }, + xField: 'x', + yField: 'y', + seriesField: 'group', + stack: true + }; +} + +/** 根据场景树图元的全局包围盒定位实际交互目标。 */ +export async function graphicCenter(page, name) { + return page.evaluate(name => { + // 读取渲染图元的实际位置,避免通过固定坐标掩盖布局变化。 + const graphic = window.__visualChart.getStage().find(node => node.name === name, true); + if (!graphic) throw new Error(`未找到交互图元 ${name}`); + const bounds = graphic.globalAABBBounds; + return { x: (bounds.x1 + bounds.x2) / 2, y: (bounds.y1 + bounds.y2) / 2 }; + }, name); +} + +/** 对照显式输入,允许已知的组件数组规范化,仍严格检查数据、回调及源轴。 */ +export function assertSpec(actual, expected, field = 'spec') { + if (['spec.crosshair', 'spec.indicator'].includes(field) && !Array.isArray(expected) && Array.isArray(actual)) { + if (actual.length !== 1) throw new Error('组件数量不正确:' + field); + actual = actual[0]; + } + if (typeof expected === 'function') { + if (typeof actual !== 'function' || actual.toString() !== expected.toString()) + throw new Error('回调配置不正确:' + field); + } else if (expected && typeof expected === 'object') { + if (!actual || typeof actual !== 'object') throw new Error('图表输入不正确:' + field); + const lengthMatches = field === 'spec.axes' ? actual.length >= expected.length : actual.length === expected.length; + if (Array.isArray(expected) && (!Array.isArray(actual) || !lengthMatches)) + throw new Error('数据长度不正确:' + field); + for (const key of Object.keys(expected)) assertSpec(actual[key], expected[key], field + '.' + key); + } else if (actual !== expected) throw new Error('图表输入不正确:' + field); +} + +/** 验证静态配置;动态回调和计算使用 verifySourceSpec 在浏览器内生成期望值。 */ +export async function verifySpec(page, expected) { + await page.evaluate(async expected => { + const { assertSpec } = await import(new URL('./helpers.mjs', location.href).href); + assertSpec(window.__visualChart.getSpec(), expected); + }, expected); +} + +/** 在同一浏览器执行冻结的源配置,避免 Node 转译函数格式和 Math 实现的微小差异。 */ +export async function verifySourceSpec(page, overrides = {}) { + await page.evaluate(async overrides => { + const { assertSpec } = await import(new URL('./helpers.mjs', location.href).href); + const { cases, loadCase } = await import(new URL('./cases/index.mjs', location.href).href); + const id = new URL(location.href).searchParams.get('case'); + const item = await loadCase(cases.find(item => item.id === id)); + assertSpec(window.__visualChart.getSpec(), { ...item.createSpec(), ...overrides }); + }, overrides); +} + +/** 检查每个系列实际绘制,以及用例关注的组件确实存在;布局由截图比较。 */ +export async function verifyRendered(page, target = 'series') { + await page.evaluate(target => { + const chart = window.__visualChart; + for (const series of chart.getChart().getAllSeries()) { + const graphics = series.getSeriesMark()?.getGraphics() ?? []; + if ( + !graphics.length || + !graphics.some(g => { + const b = g.globalAABBBounds; + return [b.x1, b.y1, b.x2, b.y2].every(Number.isFinite) && b.width() > 0 && b.height() > 0; + }) + ) + throw new Error('系列没有有效几何图元'); + } + if (target !== 'series') { + const graphic = chart + .getStage() + .find( + g => + typeof g.name === 'string' && + (target === 'label' + ? g.name === 'data-label' || g.name === 'label' + : g.name.toLowerCase().replaceAll('-', '').includes(target.replaceAll('-', ''))), + true + ); + if (!graphic || graphic.attribute.visible === false) throw new Error('缺少可见目标:' + target); + if ( + target === 'label' && + !graphic.findAll( + g => g.type === 'text' && g.attribute.visible !== false && String(g.attribute.text ?? '').length > 0, + true + ).length + ) + throw new Error('没有可见数据标签'); + } + }, target); +} + +/** 空值饼图应实际绘制占位环,而不是把没有系列图元当作通过。 */ +export async function verifyEmptyPie(page) { + await page.evaluate(() => { + const series = window.__visualChart.getChart().getAllSeries(); + if (series.length !== 1 || series[0].type !== 'pie') throw new Error('缺少饼图系列'); + const mark = series[0].getMarks().find(mark => mark.name === 'emptyCircle'); + const graphics = mark?.getGraphics() ?? []; + if ( + !graphics.some( + g => g.attribute.visible !== false && g.attribute.outerRadius > 0 && g.globalAABBBounds.width() > 0 + ) + ) + throw new Error('占位环未绘制'); + const slices = series[0].getSeriesMark().getGraphics(); + if ( + slices.some( + g => g.attribute.visible !== false && Math.abs(g.attribute.endAngle - g.attribute.startAngle) > 0.0001 + ) + ) + throw new Error('空值数据仍显示有效扇区'); + }); +} + +/** 定位指定系列图元;扇区取环内中点,再正向变换到页面坐标。 */ +export async function seriesGraphicCenter(page, name) { + return page.evaluate(name => { + // 扇区包围盒中心可能落在空心区域,不能用矩阵的逆变换代替正向坐标计算。 + const series = window.__visualChart.getChart().getAllSeries(); + const graphic = series + .flatMap(s => + s + .getMarks() + .filter(m => m.name === name) + .flatMap(m => m.getGraphics()) + ) + .find(g => g.attribute.visible !== false && g.globalAABBBounds.width() > 0); + if (!graphic) throw new Error('未找到交互系列图元 ' + name); + const a = graphic.attribute; + if (a.startAngle !== undefined) { + const angle = (a.startAngle + a.endAngle) / 2; + const radius = ((a.innerRadius || 0) + a.outerRadius) / 2; + const x = radius * Math.cos(angle), + y = radius * Math.sin(angle); + const m = graphic.globalTransMatrix; + return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f }; + } + const b = graphic.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }, name); +} diff --git a/packages/vchart/__tests__/visual/interaction-helpers.mjs b/packages/vchart/__tests__/visual/interaction-helpers.mjs new file mode 100644 index 0000000000..8d391aa76e --- /dev/null +++ b/packages/vchart/__tests__/visual/interaction-helpers.mjs @@ -0,0 +1,63 @@ +/** 按实际系列图元定位;折线取线段中点,面积取上下边界之间,避免误点相邻 symbol。 */ +export async function interactionTarget(page, name, index = 0, seriesIndex) { + return page.evaluate( + ({ name, index, seriesIndex }) => { + const series = window.__visualChart.getChart().getAllSeries(); + const selected = seriesIndex === undefined ? series : [series[seriesIndex]]; + const graphics = selected + .flatMap(s => + s + .getMarks() + .filter(m => m.name === name) + .flatMap(m => m.getGraphics()) + ) + .filter(g => g.attribute.visible !== false && g.globalAABBBounds.width() > 0); + const g = graphics[index]; + if (!g) throw new Error('缺少交互图元:' + name + '/' + index); + const a = g.attribute, + matrix = g.globalTransMatrix; + let local; + if (a.startAngle !== undefined) { + const angle = (a.startAngle + a.endAngle) / 2, + radius = ((a.innerRadius || 0) + a.outerRadius) / 2; + local = { x: radius * Math.cos(angle), y: radius * Math.sin(angle) }; + } else if (['line', 'area'].includes(name) && a.points?.length > 1) { + const p = a.points[0], + q = a.points[1]; + local = { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 }; + if (name === 'area' && Number.isFinite(p.y1) && Number.isFinite(q.y1)) + local.y = (local.y + (p.y1 + q.y1) / 2) / 2; + } + if (local) + return { + x: matrix.a * local.x + matrix.c * local.y + matrix.e, + y: matrix.b * local.x + matrix.d * local.y + matrix.f + }; + const b = g.globalAABBBounds; + return { x: (b.x1 + b.x2) / 2, y: (b.y1 + b.y2) / 2 }; + }, + { name, index, seriesIndex } + ); +} + +/** 让输入事件后的绘制队列完成;是否生效仍由独立 verify 检查真实状态。 */ +export async function interactionFrame(page) { + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); +} + +/** 保存每种系列图元的状态计数,供多步交互结束后逐步校验,不用“动作已执行”布尔值代替结果。 */ +export async function seriesStates(page) { + return page.evaluate(() => + window.__visualChart + .getChart() + .getAllSeries() + .flatMap((s, series) => + s.getMarks().map(m => { + const graphics = m.getGraphics(); + const states = {}; + for (const g of graphics) for (const state of g.currentStates ?? []) states[state] = (states[state] ?? 0) + 1; + return { series, name: m.name, count: graphics.length, states }; + }) + ) + ); +} diff --git a/packages/vchart/__tests__/visual/page.html b/packages/vchart/__tests__/visual/page.html new file mode 100644 index 0000000000..f8bb17e6fe --- /dev/null +++ b/packages/vchart/__tests__/visual/page.html @@ -0,0 +1,71 @@ + + + + + + + VChart visual regression + + + +
+ + + diff --git a/packages/vchart/__tests__/visual/playwright.config.mjs b/packages/vchart/__tests__/visual/playwright.config.mjs new file mode 100644 index 0000000000..17caa4b118 --- /dev/null +++ b/packages/vchart/__tests__/visual/playwright.config.mjs @@ -0,0 +1,37 @@ +import path from 'node:path'; +import { defineConfig } from '@playwright/test'; +import { settings } from './settings.mjs'; + +const runDir = process.env.VISUAL_RUN_DIR; +const phase = process.env.VISUAL_PHASE; +if (!runDir || !['baseline', 'current'].includes(phase)) throw new Error('请通过 visual-test.mjs 运行'); + +export default defineConfig({ + testDir: '.', + testMatch: 'visual.spec.mjs', + workers: 1, + retries: 0, + timeout: settings.timeout, + outputDir: path.join(runDir, `${phase}-results`), + snapshotPathTemplate: path.join(runDir, 'snapshots', '{arg}{ext}'), + updateSnapshots: phase === 'baseline' ? 'all' : 'none', + expect: { timeout: 5000, toMatchSnapshot: settings.comparison }, + reporter: [ + ['line'], + ['html', { outputFolder: path.join(runDir, `${phase}-report`), open: 'never' }], + ['json', { outputFile: path.join(runDir, `${phase}-playwright.json`) }], + ['./reporter.mjs'] + ], + use: { + browserName: 'chromium', + headless: true, + viewport: settings.viewport, + deviceScaleFactor: settings.deviceScaleFactor, + locale: settings.locale, + timezoneId: settings.timezoneId, + colorScheme: 'light', + reducedMotion: 'reduce', + serviceWorkers: 'block', + trace: 'retain-on-failure' + } +}); diff --git a/packages/vchart/__tests__/visual/report.mjs b/packages/vchart/__tests__/visual/report.mjs new file mode 100644 index 0000000000..e3f01fdacc --- /dev/null +++ b/packages/vchart/__tests__/visual/report.mjs @@ -0,0 +1,442 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/** 转义报告中的动态文本,同时用于 HTML 内容和属性。 */ +function escape(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +/** 复现参数以单引号包裹,避免目录或报告文字被 shell 解释。 */ +function quote(value) { + return `'${String(value).replaceAll("'", "'\\''")}'`; +} + +/** 人与 Agent 共用范围说明;未选用例不被描述为已通过。 */ +function selectionDescription(report) { + const selection = report.selection; + if (!selection) return `范围未记录;本次记录 ${report.cases.length} 个用例`; + const label = + selection.mode === 'all' + ? '全部本地用例' + : `${selection.mode === 'directory' ? '目录' : '单用例'} ${selection.value}`; + return `${label};选中 ${selection.selectedCount} / ${selection.totalCount};其余 ${ + selection.totalCount - selection.selectedCount + } 个未执行`; +} + +/** 将已存在的运行内文件转换为可搬迁的相对路径,拒绝越界引用。 */ +async function artifact(runDir, relative) { + if (!relative || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) return null; + try { + return (await fs.stat(path.join(runDir, relative))).isFile() ? relative : null; + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +/** 汇合两阶段结果;缺少图片或未完成比较不能被解释为通过。 */ +export async function saveReport(runDir, summary) { + const phases = {}; + const reportStarted = performance.now(); + const issues = [...(summary.issues ?? [])]; + const selection = summary.selection; + if ( + selection && + (!['all', 'directory', 'case'].includes(selection.mode) || + !Number.isSafeInteger(selection.totalCount) || + selection.totalCount < summary.cases.length || + selection.selectedCount !== summary.cases.length || + !Array.isArray(selection.selectedIds) || + selection.selectedIds.length !== summary.cases.length || + new Set(selection.selectedIds).size !== summary.cases.length || + summary.cases.some(item => !selection.selectedIds.includes(item.id)) || + (selection.mode === 'all' && (selection.value !== null || selection.totalCount !== selection.selectedCount)) || + (selection.mode !== 'all' && typeof selection.value !== 'string') || + (selection.mode === 'case' && (selection.selectedCount !== 1 || selection.selectedIds[0] !== selection.value))) + ) + issues.push({ + phase: 'report', + category: 'execution', + code: 'RESULT_SET_MISMATCH', + message: '报告范围与选中用例集合不一致' + }); + for (const phase of ['baseline', 'current']) { + phases[phase] = summary[`${phase}Result`]; + if (!phases[phase]) { + try { + phases[phase] = JSON.parse(await fs.readFile(path.join(runDir, `${phase}-result.json`), 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') + issues.push({ phase, category: 'report', code: 'REPORT_FAILED', message: String(error) }); + } + } + for (const message of phases[phase]?.errors ?? []) + issues.push({ phase, category: 'execution', code: 'EXECUTION_FAILED', message }); + } + if (summary.error) + issues.push({ phase: summary.stage ?? 'preparation', category: 'execution', message: summary.error }); + if (summary.cleanupError) issues.push({ phase: 'cleanup', category: 'execution', message: summary.cleanupError }); + for (const [phase, result] of Object.entries(phases)) { + if (!result) continue; + const expected = summary.cases.map(item => item.id); + const actual = result.tests.map(item => item.id); + if ( + actual.length !== expected.length || + new Set(actual).size !== actual.length || + actual.some(id => !expected.includes(id)) + ) + issues.push({ phase, category: 'execution', code: 'RESULT_SET_MISMATCH', message: '阶段结果与选择集不一致' }); + if (result.finalized === false) + issues.push({ phase, category: 'execution', code: 'RUN_INCOMPLETE', message: '阶段异常结束,保留已完成用例' }); + } + // 预检失败时尚未解析基线,复现仍应保留用户请求的 SHA 或自比较模式。 + const baselineSha = summary.baseline?.sha ?? summary.request?.baseline; + const baselineArg = + summary.baseline?.repository === 'working-tree' || summary.request?.['self-compare'] + ? '--self-compare' + : baselineSha + ? `--baseline ${baselineSha}` + : ''; + const cases = []; + for (const item of summary.cases) { + const baseline = phases.baseline?.tests.find(test => test.id === item.id); + const current = phases.current?.tests.find(test => test.id === item.id); + const errors = []; + const images = { baseline: null, current: null, diff: null }; + for (const [phase, result] of [ + ['baseline', baseline], + ['current', current] + ]) { + for (const message of result?.errors ?? []) { + errors.push({ + phase, + stage: result.stage ?? 'execution', + category: result.status === 'diff' ? 'visual_difference' : result.category ?? 'execution', + code: result.code ?? (result.status === 'diff' ? 'VISUAL_DIFFERENCE' : 'EXECUTION_FAILED'), + message + }); + } + images[phase] = await artifact(runDir, result?.attachments?.find(file => file.name === 'rendered')?.path); + if (['passed', 'diff'].includes(result?.status) && !images[phase]) { + errors.push({ + phase, + stage: 'report', + category: 'missing_artifact', + code: 'MISSING_ARTIFACT', + message: '缺少最终截图' + }); + } + } + images.diff = await artifact(runDir, current?.attachments?.find(file => file.name.endsWith('-diff.png'))?.path); + if (current?.status === 'diff' && !images.diff) + errors.push({ + phase: 'current', + stage: 'report', + category: 'missing_artifact', + code: 'MISSING_ARTIFACT', + message: '缺少差异图' + }); + const executionError = + errors.some(error => error.category !== 'visual_difference') || + [baseline, current].some(result => result?.status === 'error'); + const status = executionError + ? 'error' + : baseline?.status !== 'passed' || !current || current.status === 'not_run' + ? 'not_run' + : current.status; + const attachments = []; + for (const [phase, result] of [ + ['baseline', baseline], + ['current', current] + ]) { + for (const file of result?.attachments ?? []) { + const relative = await artifact(runDir, file.path); + if (relative) attachments.push({ phase, ...file, path: relative }); + } + } + cases.push({ + ...item, + status, + ...(status === 'not_run' + ? { + blockedBy: { + phase: baseline?.status === 'passed' ? 'current' : 'baseline', + reason: '前序阶段失败或该阶段未完成' + } + } + : {}), + phases: { baseline: baseline?.status ?? 'not_run', current: current?.status ?? 'not_run' }, + durationMs: (baseline?.durationMs ?? 0) + (current?.durationMs ?? 0), + images, + attachments, + errors, + rerun: `node packages/vchart/scripts/visual-test.mjs ${baselineArg} --case ${quote(item.id)}`.replace(' ', ' ') + }); + } + const counts = Object.fromEntries( + ['passed', 'diff', 'error', 'not_run'].map(status => [status, cases.filter(item => item.status === status).length]) + ); + const complete = + cases.length > 0 && + cases.every(item => ['passed', 'diff'].includes(item.status)) && + !issues.some(issue => ['RESULT_SET_MISMATCH', 'RUN_INCOMPLETE'].includes(issue.code)); + const status = summary.status === 'error' || issues.length || !complete ? 'error' : counts.diff ? 'diff' : 'passed'; + const logs = {}; + for (const name of [ + 'build.log', + 'baseline.log', + 'current.log', + 'baseline-report/index.html', + 'current-report/index.html' + ]) { + const relative = await artifact(runDir, name); + if (relative) logs[name] = relative; + } + const { baselineResult, currentResult, ...metadata } = summary; + const report = { + ...metadata, + schemaVersion: 1, + finalized: summary.finalized ?? true, + status, + complete, + counts, + cases, + issues, + logs, + rerun: selection + ? [ + 'node packages/vchart/scripts/visual-test.mjs', + baselineArg, + selection.mode === 'all' + ? '' + : `${selection.mode === 'directory' ? '--dir' : '--case'} ${quote(selection.value)}` + ] + .filter(Boolean) + .join(' ') + : null, + environment: { + ...summary.environment, + browser: phases.current?.browser ?? phases.baseline?.browser ?? summary.environment.browser + } + }; + // 汇总只生成新对象;HTML 和 Markdown 不改变调用方的状态。 + report.timings = { ...summary.timings, reportMs: Math.round(performance.now() - reportStarted) }; + await writeJson(path.join(runDir, 'summary.json'), report); + await fs.writeFile(path.join(runDir, 'agent-summary.md'), agentSummary(report)); + await fs.writeFile(path.join(runDir, 'index.html'), renderHtml(report)); + report.timings.reportMs = Math.round(performance.now() - reportStarted); + if (report.timings.totalMs !== undefined) report.timings.totalMs += report.timings.reportMs; + await writeJson(path.join(runDir, 'summary.json'), report); + await fs.writeFile(path.join(runDir, 'agent-summary.md'), agentSummary(report)); + await fs.writeFile(path.join(runDir, 'index.html'), renderHtml(report)); + return report; +} + +/** 输出失败优先的纯文本入口,让 Agent 按路径读取证据而非消耗 Base64。 */ +function agentSummary(report) { + const lines = [ + '# VChart 视觉回归结果', + '', + `状态:${report.status};完整比较:${report.complete};${JSON.stringify(report.counts)}`, + `测试范围:${selectionDescription(report)}`, + ...(report.rerun ? [`本次范围复现:\`${report.rerun}\``] : []), + '', + `基线:${report.baseline?.repository ?? '未解析'} @ ${report.baseline?.sha ?? '未解析'}`, + `本地:${report.local?.head ?? '未知'};dirty=${report.local?.dirty ?? '未知'};工作区摘要=${ + report.local?.digest ?? '未知' + }`, + `环境:${JSON.stringify(report.environment)}`, + '', + '完整结构化结果:[summary.json](summary.json)(schemaVersion=1);所有证据路径相对于此报告目录。', + '以下记录测试事实,不判断差异是否属于缺陷。复现命令在仓库根目录执行;基线 SHA 固定,本地工作区需要与摘要一致。', + '用例 source.path 指向仓库文件;source.frozenPath 指向本次运行冻结文件。', + '截图需由支持图片的 Agent 按需打开;本报告不调用模型,也不自动接受差异。', + '' + ]; + for (const issue of report.issues) + lines.push( + `## 运行错误:${issue.phase} / ${issue.code ?? issue.category}`, + '', + ...issue.message.split('\n').map(line => `> ${line}`), + '' + ); + for (const item of report.cases.filter(item => item.status !== 'passed')) { + lines.push( + `## ${item.id} — ${item.status}`, + '', + item.purpose, + '', + `源码:${item.source?.path ?? '未知'}:${item.source?.line ?? '?'};冻结副本:${ + item.source?.frozenPath ?? '未知' + }`, + `阶段:${JSON.stringify(item.phases)}`, + '', + `复现:\`${item.rerun}\``, + '' + ); + for (const [kind, file] of Object.entries(item.images)) + lines.push(`- ${kind}:${file ? `[${file}](${file})` : '未生成或缺失'}`); + for (const error of item.errors) + lines.push( + '', + `${error.phase} / ${error.stage} / ${error.code ?? error.category}`, + ...error.message.split('\n').map(line => `> ${line}`) + ); + lines.push(''); + } + if (report.complete && !report.counts.diff) lines.push('所选用例全部通过,详情见 summary.json。', ''); + lines.push('## 日志与详细报告', '', ...Object.values(report.logs).map(file => `- [${file}](${file})`), ''); + return lines.join('\n'); +} + +/** 生成无需服务和网络的三图报告;原图链接保留完整分辨率。 */ +function renderHtml(report) { + const labels = { passed: '通过', diff: '视觉差异', error: '执行错误', not_run: '未完成' }; + const link = (file, label) => `${escape(label)}`; + const baseline = report.baseline?.repository === 'working-tree' ? '工作区自比较' : '官方基线'; + const cards = report.cases + .map( + item => `
+

${escape(item.id)}

${ + labels[item.status] + }${(item.durationMs / 1000).toFixed(1)}s
+

${escape(item.purpose)}

${[ + ['baseline', `${baseline} · ${report.baseline?.sha?.slice(0, 8) ?? '?'}`], + ['current', `本地 · ${report.local?.head?.slice(0, 8) ?? '?'}${report.local?.dirty ? ' + 未提交修改' : ''}`], + ['diff', 'Diff · 差异图'] + ] + .map( + ([kind, title]) => + `
${escape(title)}
${ + item.images[kind] + ? `${escape(`${item.id} ${title}`)}` + : `
${ + kind === 'diff' && item.status === 'passed' ? '无视觉差异' : '未生成或缺失;请查看状态与诊断' + }
` + }
` + ) + .join('')}
+

用例:${escape(item.source?.path)}:${item.source?.line ?? '?'} · ${ + item.source?.frozenPath ? link(item.source.frozenPath, '查看冻结用例') : '' + } · baseline=${item.phases.baseline} / current=${item.phases.current}

+
${escape(item.rerun)}
+ ${ + item.errors.length + ? `
诊断(${ + item.errors.length + })${item.errors + .map( + error => + `

${escape(`${error.phase} / ${error.stage} / ${error.code ?? error.category}`)}

${escape(
+                  error.message
+                )}
` + ) + .join('')}
` + : '' + } + ${ + item.attachments.length + ? `
证据文件
    ${item.attachments + .map(file => `
  • ${escape(file.phase)} · ${link(file.path, file.name)}
  • `) + .join('')}
` + : '' + }
` + ) + .join(''); + return `VChart 视觉回归 · ${ + labels[report.status] + } +

VChart 视觉回归 · ${labels[report.status]}

${escape(report.startedAt)} · ${ + report.complete ? '所选用例完成比较' : '比较未完整完成' + } · ${escape(report.environment.platform)} / ${escape(report.environment.arch)} · Chromium ${escape( + report.environment.browser ?? '未启动' + )}

基线 ${escape(report.baseline?.sha ?? '未解析')} · 本地 ${escape(report.local?.head ?? '未解析')}${ + report.local?.dirty ? '(有未提交修改)' : '' + }

${escape(selectionDescription(report))}

+
${Object.entries(report.counts) + .map(([status, count]) => `
${count}${labels[status]}
`) + .join('')}
+

同机、同一套用例对比。点击图片打开原图;视觉差异需要审查,不自动判为缺陷。

+ ${ + report.rerun + ? `

本次范围复现:

${escape( + report.rerun + )}
` + : '' + } + ${report.issues + .map( + issue => + `
${escape( + `${issue.phase} / ${issue.code ?? issue.category}` + )}
${escape(issue.message)}
` + ) + .join('')} +
${cards} +
+ `; +} + +/** 在报告目录内原子替换 JSON,避免读者看到半写入的终态。 */ +async function writeJson(file, value) { + const temporary = `${file}.tmp`; + await fs.writeFile(temporary, JSON.stringify(value, null, 2)); + await fs.rename(temporary, file); +} diff --git a/packages/vchart/__tests__/visual/reporter.mjs b/packages/vchart/__tests__/visual/reporter.mjs new file mode 100644 index 0000000000..153dfc10bd --- /dev/null +++ b/packages/vchart/__tests__/visual/reporter.mjs @@ -0,0 +1,90 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { stripVTControlCharacters } from 'node:util'; + +/** 每个完成用例即保存证据,不依赖进程正常结束才落盘。 */ +export default class VisualReporter { + tests = []; + errors = []; + onError(error) { + // 记录 worker 等运行级错误,并保留已完成用例。 + this.errors.push(stripVTControlCharacters(error.message ?? String(error))); + this.persist(false); + } + onTestEnd(test, result) { + // diff 标记仅由截图适配层在确认原生比较证据完整后添加。 + const difference = result.attachments.some(item => item.name === 'visual-difference'); + const stage = test.annotations.find(item => item.type === 'stage')?.description ?? 'setup'; + const category = + result.status === 'timedOut' + ? 'timeout' + : test.annotations.find(item => item.type === 'failure-category')?.description || stage; + const codes = { + resource: 'RESOURCE_FAILED', + render: 'RENDER_FAILED', + interaction: 'INTERACTION_ASSERTION_FAILED', + verify: 'CASE_ASSERTION_FAILED', + screenshot: 'SCREENSHOT_FAILED', + comparison: 'COMPARISON_FAILED', + timeout: 'TIMEOUT', + missing_artifact: 'MISSING_ARTIFACT', + setup: 'PREFLIGHT_FAILED' + }; + this.browser = test.annotations.find(item => item.type === 'browser')?.description ?? this.browser; + this.tests.push({ + id: test.title, + status: + result.status === 'passed' + ? 'passed' + : result.status === 'skipped' + ? 'not_run' + : result.status === 'failed' && difference + ? 'diff' + : 'error', + stage, + category, + code: + result.status === 'passed' + ? null + : result.status === 'skipped' + ? 'RUN_INCOMPLETE' + : difference + ? 'VISUAL_DIFFERENCE' + : codes[category] ?? 'EXECUTION_FAILED', + durationMs: result.duration, + errors: result.errors.map(error => stripVTControlCharacters(error.message ?? String(error))), + attachments: result.attachments + .filter(item => item.path) + .map(item => ({ + name: item.name, + contentType: item.contentType, + path: path.relative(process.env.VISUAL_RUN_DIR, item.path).split(path.sep).join('/') + })) + }); + this.persist(false); + } + persist(finalized, endedStatus) { + // 原子替换阶段结果;未终结的阶段始终不宣称完整成功。 + const status = + !finalized || + this.errors.length || + !this.tests.length || + this.tests.some(test => ['error', 'not_run'].includes(test.status)) || + ['interrupted', 'timedout'].includes(endedStatus) + ? 'error' + : this.tests.some(test => test.status === 'diff') + ? 'diff' + : 'passed'; + const file = path.join(process.env.VISUAL_RUN_DIR, `${process.env.VISUAL_PHASE}-result.json`); + const temporary = `${file}.tmp`; + fs.writeFileSync( + temporary, + JSON.stringify({ finalized, status, browser: this.browser, tests: this.tests, errors: this.errors }, null, 2) + ); + fs.renameSync(temporary, file); + } + onEnd(result) { + // 最终更新状态,保留每个用例独立写入的原始诊断。 + this.persist(true, result.status); + } +} diff --git a/packages/vchart/__tests__/visual/settings.mjs b/packages/vchart/__tests__/visual/settings.mjs new file mode 100644 index 0000000000..e10123d2d0 --- /dev/null +++ b/packages/vchart/__tests__/visual/settings.mjs @@ -0,0 +1,12 @@ +/** 两阶段共用的确定性环境与像素比较策略。 */ +export const settings = { + viewport: { width: 1000, height: 800 }, + chart: { width: 800, height: 600 }, + deviceScaleFactor: 1, + locale: 'en-US', + timezoneId: 'UTC', + fontFamily: 'Arial', + timeout: 30000, + stableTimeout: 5000, + comparison: { threshold: 0.1, maxDiffPixels: 0 } +}; diff --git a/packages/vchart/__tests__/visual/visual.spec.mjs b/packages/vchart/__tests__/visual/visual.spec.mjs new file mode 100644 index 0000000000..d476797c1b --- /dev/null +++ b/packages/vchart/__tests__/visual/visual.spec.mjs @@ -0,0 +1,146 @@ +import fs from 'node:fs/promises'; +import { test, expect } from '@playwright/test'; +import { cases, loadCase } from './cases/index.mjs'; +import { settings } from './settings.mjs'; + +/** 等待两张连续截图完全一致;持续变化视为执行错误而非像素差异。 */ +async function stableScreenshot(page) { + let previous; + const deadline = Date.now() + settings.stableTimeout; + while (Date.now() < deadline) { + const current = await page.screenshot({ animations: 'disabled', timeout: 5000 }); + if (previous?.equals(current)) return current; + previous = current; + await page.waitForTimeout(100); + } + throw new Error('截图持续变化,5 秒内未稳定'); +} + +/** 检查渲染错误和有效画布内容,拒绝两侧同时空白造成的假通过。 */ +async function checkDrawing(page, errors) { + if (errors.length) throw new Error(errors.join('\n')); + await page.evaluate(() => { + // 图表绘制必须产生非透明、非白色像素。 + if (window.__visualErrors.length) throw new Error(window.__visualErrors.join('\n')); + const canvas = window.__visualChart?.getCanvas(); + if (!canvas || canvas.width <= 0 || canvas.height <= 0) throw new Error('没有有效画布'); + const series = window.__visualChart.getChart().getAllSeries(); + const hasData = series.some(item => item.getSeriesMark()?.getGraphics()?.length > 0); + // 显式空值饼图可用真实占位环替代数据扇区,不能仅凭坐标轴或标签放行空图。 + const hasPlaceholder = series.some( + item => + item.type === 'pie' && + item.getSpec().emptyPlaceholder?.showEmptyCircle && + item + .getMarks() + .find(mark => mark.name === 'emptyCircle') + ?.getGraphics() + .some(g => g.attribute.visible !== false && g.attribute.outerRadius > 0 && g.globalAABBBounds.width() > 0) + ); + if (!hasData && !hasPlaceholder) throw new Error('没有绘制数据图元'); + const context = canvas.getContext('2d'); + const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; + let ink = 0; + for (let i = 0; i < pixels.length; i += 4) + if (pixels[i + 3] && (pixels[i] < 245 || pixels[i + 1] < 245 || pixels[i + 2] < 245)) ink++; + if (ink < 100) throw new Error('图表没有有效绘制'); + }); +} + +const selectedIds = JSON.parse(process.env.VISUAL_CASE_IDS); +for (const metadata of cases.filter(item => selectedIds.includes(item.id))) { + const item = { ...metadata, ...(await loadCase(metadata)) }; + test(item.id, async ({ page, browser }, info) => { + // 记录来源和浏览器版本,并收集整个用例生命周期内的异常。 + info.annotations.push( + { type: 'purpose', description: item.purpose }, + { type: 'browser', description: browser.version() } + ); + const stage = { type: 'stage', description: 'resource' }; + info.annotations.push(stage); + const failure = { type: 'failure-category', description: '' }; + info.annotations.push(failure); + const errors = []; + const base = process.env.VISUAL_BASE_URL; + page.on('pageerror', error => { + failure.description = 'render'; + errors.push(error.message); + }); + page.on('requestfailed', request => { + failure.description = 'resource'; + errors.push(`资源失败:${request.url()} ${request.failure()?.errorText}`); + }); + page.on('response', response => { + if (response.status() >= 400) { + failure.description = 'resource'; + errors.push(`HTTP ${response.status()}:${response.url()}`); + } + }); + await page.route('**/*', route => { + // 截图阶段禁止 CDN 和内网请求。 + if (new URL(route.request().url()).origin === base) return route.continue(); + failure.description = 'resource'; + errors.push(`禁止外部请求:${route.request().url()}`); + return route.abort(); + }); + try { + await page.goto(`${base}/suite/page.html?phase=${process.env.VISUAL_PHASE}&case=${item.id}`); + stage.description = 'render'; + await page.waitForFunction(() => window.__visualReady || window.__visualErrors?.length); + await checkDrawing(page, errors); + stage.description = 'interaction'; + await item.exercise?.(page); + stage.description = item.exercise ? 'interaction' : 'verify'; + await item.verify(page); + stage.description = 'screenshot'; + await page.evaluate(() => document.fonts.ready); + const screenshot = await stableScreenshot(page); + await checkDrawing(page, errors); + const renderedPath = info.outputPath('rendered.png'); + await fs.writeFile(renderedPath, screenshot); + await info.attach('rendered', { path: renderedPath, contentType: 'image/png' }); + stage.description = 'comparison'; + const name = `${item.id}.png`; + if (process.env.VISUAL_PHASE === 'current') { + // 缺失基线必须在图片断言之前归类为执行错误。 + try { + await fs.access(info.snapshotPath(name)); + } catch (error) { + failure.description = 'missing_artifact'; + throw error; + } + } + try { + expect(screenshot).toMatchSnapshot(name); + } catch (error) { + if (process.env.VISUAL_PHASE === 'current' && (await hasComparisonEvidence(info, name))) + await info.attach('visual-difference', { + body: Buffer.from('图像存在差异,请检查是否符合预期'), + contentType: 'text/plain' + }); + throw error; + } + } finally { + // 显式释放图表,浏览器 context 由 Playwright fixture 自动回收。 + await page.evaluate(() => window.__visualChart?.release()).catch(() => {}); + } + }); +} + +/** 只有原生比较生成完整 PNG 证据时才允许把断言失败归类为视觉差异。 */ +async function hasComparisonEvidence(info, name) { + const stem = name.slice(0, -4); + const dimensions = []; + for (const kind of ['expected', 'actual', 'diff']) { + const file = info.attachments.find(item => item.name === `${stem}-${kind}.png`)?.path; + if (!file) return false; + try { + const bytes = await fs.readFile(file); + if (bytes.length < 24 || bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') return false; + dimensions.push(`${bytes.readUInt32BE(16)}x${bytes.readUInt32BE(20)}`); + } catch { + return false; + } + } + return new Set(dimensions).size === 1; +} diff --git a/packages/vchart/package.json b/packages/vchart/package.json index a730c82bfd..5b3cc9fe2c 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -49,7 +49,8 @@ "test-live": "npm run test-watch __tests__/unit", "test-watch": "cross-env DEBUG_MODE=1 jest --watch", "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts", - "size": "size-limit" + "size": "size-limit", + "test:visual": "node scripts/visual-test.mjs" }, "files": [ "esm", @@ -121,7 +122,8 @@ "size-limit": "9.0.0", "@size-limit/file": "9.0.0", "rimraf": "3.0.2", - "cross-env": "^7.0.3" + "cross-env": "^7.0.3", + "@playwright/test": "1.63.0" }, "dependencies": { "@visactor/vutils": "~1.0.24", diff --git a/packages/vchart/scripts/visual-test.mjs b/packages/vchart/scripts/visual-test.mjs new file mode 100644 index 0000000000..bbb36778cf --- /dev/null +++ b/packages/vchart/scripts/visual-test.mjs @@ -0,0 +1,86 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; +import { createRuntime, fault } from './visual/runtime.mjs'; +import { loadCases, preflight, runVisual } from './visual/runner.mjs'; + +/** 解析公共命令;帮助不加载 Playwright,非法选项仍明确报错。 */ +export function parseOptions(args) { + let values, tokens; + try { + ({ values, tokens } = parseArgs({ + args, + tokens: true, + options: { + baseline: { type: 'string' }, + case: { type: 'string' }, + dir: { type: 'string' }, + 'self-compare': { type: 'boolean' }, + list: { type: 'boolean' }, + check: { type: 'boolean' }, + help: { type: 'boolean' } + } + })); + } catch (error) { + throw fault('INVALID_ARGUMENT', error.message, error); + } + if (values.help) return values; + if (tokens.filter(token => token.kind === 'option' && token.name === 'dir').length > 1) + throw fault('INVALID_ARGUMENT', '--dir 只能指定一次'); + if (values.dir !== undefined && !/^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)*$/.test(values.dir)) + throw fault('INVALID_ARGUMENT', '--dir 需要 cases 内的相对目录,例如 components/label'); + if (values.dir !== undefined && values.case !== undefined) throw fault('INVALID_ARGUMENT', '--dir 与 --case 互斥'); + if ( + (values.list || values.check) && + ((values.list && values.check) || + values.baseline !== undefined || + values.case !== undefined || + values.dir !== undefined || + values['self-compare']) + ) + throw fault('INVALID_ARGUMENT', '--list/--check 必须独立使用'); + if (values.baseline !== undefined && !/^[a-f0-9]{40}$/i.test(values.baseline)) + throw fault('INVALID_ARGUMENT', '--baseline 需要完整 40 位 SHA'); + if (values.baseline && values['self-compare']) throw fault('INVALID_ARGUMENT', '--baseline 与 --self-compare 互斥'); + if (values.case !== undefined && !/^[a-z][a-z0-9-]*$/.test(values.case)) + throw fault('INVALID_ARGUMENT', '--case 需要有效用例 ID'); + return values; +} +/** 入口只管理参数、预检模式和运行生命周期。 */ +async function main() { + const options = parseOptions(process.argv.slice(2)); + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); + if (options.help) { + console.log( + 'node packages/vchart/scripts/visual-test.mjs [--baseline <40位SHA> | --self-compare] [--dir <目录> | --case ]\n默认运行全部本地用例;--dir 递归运行 cases 内的一个目录(例如 components/label)。\n--list 列举用例;--check 本机环境检查;--help 帮助\n默认:官方 VisActor/VChart develop。退出码:0 通过,1 视觉差异,2 执行错误。' + ); + return; + } + if (options.list || options.check) { + const cases = await loadCases(path.join(root, 'packages/vchart/__tests__/visual')); + if (options.list) { + console.log( + cases.map(item => `${item.id}\t${item.purpose}\t${item.file}\t${item.sourceExample ?? ''}`).join('\n') + ); + return; + } + await fs.mkdir(path.join(root, '.vchart-visual'), { recursive: true }); + const runtime = createRuntime(); + const detach = runtime.listenSignals(); + try { + console.log(JSON.stringify({ status: 'passed', ...(await preflight(root, runtime)), cases: cases.length })); + } finally { + const errors = await runtime.cleanup(); + detach(); + if (errors.length) throw fault('CLEANUP_FAILED', JSON.stringify(errors)); + } + return; + } + process.exitCode = await runVisual(root, options); +} +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) + main().catch(error => { + console.error(`${error.code ?? 'EXECUTION_FAILED'}: ${error.message}`); + process.exitCode = 2; + }); diff --git a/packages/vchart/scripts/visual-test.test.mjs b/packages/vchart/scripts/visual-test.test.mjs new file mode 100644 index 0000000000..ae1085f3bf --- /dev/null +++ b/packages/vchart/scripts/visual-test.test.mjs @@ -0,0 +1,840 @@ +import { test } from 'node:test'; +import { runInNewContext } from 'node:vm'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; +import { parseOptions } from './visual-test.mjs'; +import { + executePhase as runPhase, + loadCases, + selectCases, + validateResults, + acquireLock, + runVisual +} from './visual/runner.mjs'; +import { cases as caseMetadata } from '../__tests__/visual/cases/index.mjs'; +import { createRuntime, serve, fileManifest, cleanupAll } from './visual/runtime.mjs'; +import { readCache, publishCache, build, workingTree } from './visual/build.mjs'; +const runtime = createRuntime(); +/** 测试从冻结清单选择用例,使用与 CLI 相同的阶段入口。 */ +async function executePhase(dir, phase, url, id) { + const selected = await loadCases(path.join(dir, 'suite'), id); + return runPhase( + root, + dir, + phase, + url, + selected.map(item => item.id), + runtime + ); +} +/** 通过正式进程管理器验证命令失败。 */ +const run = (...args) => runtime.run(...args); +import { saveReport } from '../__tests__/visual/report.mjs'; +import { createRequire } from 'node:module'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); +const packageDir = path.join(root, 'packages/vchart'); + +test('CLI rejects unknown cases and conflicting baseline options', () => { + // 参数错误必须在构建之前返回约定的执行错误退出码。 + for (const args of [ + ['--case', 'missing'], + ['--case', ''], + ['--dir', 'missing'], + ['--dir', 'components/empty'], + ['--dir', ''], + ['--dir', '/components'], + ['--dir', '../components'], + ['--dir', 'components/../charts'], + ['--dir', 'components/*'], + ['--dir', 'components\\label'], + ['--dir', 'components', '--case', 'pie-label'], + ['--dir', 'components', '--dir=charts'], + ['--list', '--dir', 'components'], + ['--check', '--dir', 'components'], + ['--baseline', ''], + ['--baseline', 'a'.repeat(40), '--self-compare'], + ['--update-snapshots'], + ['--list', '--case', 'bar-stack'], + ['--check', '--self-compare'], + ['--check', '--list'] + ]) { + const result = spawnSync(process.execPath, [path.join(packageDir, 'scripts/visual-test.mjs'), ...args]); + assert.equal(result.status, 2, result.stderr.toString()); + assert.ok(!result.stdout.toString().includes('构建当前工作区'), '选择错误不得进入构建'); + } +}); + +test( + 'real screenshots distinguish differences, missing inputs and execution failures', + { timeout: 180000 }, + async t => { + // 使用已构建的真实 VChart,在独立目录中注入故障,不修改源码或正式报告。 + const bundle = await fs.readFile(path.join(packageDir, 'build/index.js')); + const output = path.join(root, '.vchart-visual'); + await fs.mkdir(output, { recursive: true }); + const dir = await fs.mkdtemp(path.join(output, 'verify-')); + await fs.cp(path.join(packageDir, '__tests__/visual'), path.join(dir, 'suite'), { recursive: true }); + await fs.symlink(path.join(packageDir, 'node_modules'), path.join(dir, 'node_modules'), 'dir'); + await fs.writeFile(path.join(dir, 'baseline.js'), bundle); + await fs.writeFile(path.join(dir, 'current.js'), bundle); + const { server, url } = await serve(dir); + const barFile = caseMetadata.find(item => item.id === 'bar-stack').file.slice(2); + // 真实阶段产物用于验证三图与结构化报告保持一致。 + const summary = () => ({ + status: 'passed', + environment: {}, + baseline: { repository: 'official', sha: 'a'.repeat(40) }, + local: { head: 'b'.repeat(40), dirty: true }, + selection: { + mode: 'directory', + value: 'charts/bar', + selectedIds: ['bar-stack'], + selectedCount: 1, + totalCount: caseMetadata.length + }, + cases: [ + { + id: 'bar-stack', + purpose: '颜色对比', + source: { + path: `packages/vchart/__tests__/visual/cases/${barFile}`, + line: 1, + frozenPath: `suite/cases/${barFile}` + } + } + ] + }); + try { + await t.test('identical bundle passes', async () => { + // 同一构建分别生成基线和本地图像。 + assert.equal((await executePhase(dir, 'baseline', url, 'bar-stack')).status, 'passed'); + assert.equal((await executePhase(dir, 'current', url, 'bar-stack')).status, 'passed'); + }); + await t.test('scope reports preserve exact selection and reject inconsistent metadata', async () => { + // 同一真实图片用于检查范围协议,不把未选用例计为未完成或已通过。 + for (const selection of [ + summary().selection, + { mode: 'case', value: 'bar-stack', selectedIds: ['bar-stack'], selectedCount: 1, totalCount: 10 }, + { mode: 'all', value: null, selectedIds: ['bar-stack'], selectedCount: 1, totalCount: 1 } + ]) { + const report = await saveReport(dir, { ...summary(), selection, baseline: { repository: 'working-tree' } }); + assert.equal(report.status, 'passed'); + assert.equal(report.complete, true); + assert.deepEqual(report.selection, selection); + assert.equal(report.counts.passed, 1); + assert.equal(report.counts.not_run, 0); + assert.match(report.rerun, /--self-compare/); + assert.ok(!report.rerun.includes('--baseline')); + const flag = + selection.mode === 'directory' + ? "--dir 'charts/bar'" + : selection.mode === 'case' + ? "--case 'bar-stack'" + : ''; + assert.equal( + report.rerun, + `node packages/vchart/scripts/visual-test.mjs --self-compare${flag ? ` ${flag}` : ''}` + ); + const expected = `选中 1 / ${selection.totalCount};其余 ${selection.totalCount - 1} 个未执行`; + assert.ok((await fs.readFile(path.join(dir, 'index.html'), 'utf8')).includes(expected)); + assert.ok((await fs.readFile(path.join(dir, 'agent-summary.md'), 'utf8')).includes(expected)); + await fs.access(path.join(dir, report.cases[0].source.frozenPath)); + } + for (const override of [ + { selectedIds: ['unknown'] }, + { selectedCount: 2 }, + { totalCount: 0 }, + { mode: 'all', value: null } + ]) { + const report = await saveReport(dir, { ...summary(), selection: { ...summary().selection, ...override } }); + assert.equal(report.status, 'error'); + assert.equal(report.complete, false); + assert.ok(report.issues.some(issue => issue.code === 'RESULT_SET_MISMATCH')); + } + }); + await t.test('candidate-only color change is a visual difference', async () => { + // 只修改候选构建的行为,两侧用例代码保持完全一致。 + for (const change of [ + 'color: ["#e00000", "#00a000"]', + 'padding: {left: 120, right: 20, top: 20, bottom: 20}', + 'label: {visible: true}' + ]) { + await fs.writeFile( + path.join(dir, 'current.js'), + Buffer.concat([ + bundle, + Buffer.from( + `\nconst Original = VChart.default; VChart.default = class extends Original { constructor(spec, options) { super({...spec, ${change}}, options); } };` + ) + ]) + ); + assert.equal((await executePhase(dir, 'current', url, 'bar-stack')).status, 'diff', change); + } + const input = summary(); + const report = await saveReport(dir, input); + assert.equal(input.status, 'passed', '汇总不能修改输入状态'); + assert.equal(report.status, 'diff'); + assert.equal(report.counts.diff, 1); + assert.equal(report.schemaVersion, 1); + for (const file of Object.values(report.cases[0].images)) { + assert.ok(file, JSON.stringify(report.cases[0])); + assert.ok(!path.isAbsolute(file)); + await fs.access(path.join(dir, file)); + } + assert.match(report.cases[0].rerun, /--baseline a{40} --case 'bar-stack'/); + assert.match(report.rerun, /--baseline a{40} --dir 'charts\/bar'/); + assert.ok(!JSON.stringify(report).includes('base64')); + const markdown = await fs.readFile(path.join(dir, 'agent-summary.md'), 'utf8'); + assert.match(markdown, /bar-stack — diff/); + // file:// 打开整个报告,确认无需 HTTP 服务,筛选和图片均可用。 + const require = createRequire(path.join(packageDir, 'package.json')); + const { chromium } = require('@playwright/test'); + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + const unexpected = []; + page.on('request', request => { + if (!request.url().startsWith('file:')) unexpected.push(request.url()); + }); + page.on('pageerror', error => unexpected.push(error.message)); + await page.goto(new URL(`file://${dir}/index.html`).href); + assert.ok( + (await page.locator('[data-testid="selection"]').textContent()).includes(`选中 1 / ${caseMetadata.length}`) + ); + assert.match(await page.locator('header').textContent(), /所选用例完成比较/); + assert.equal( + await page.locator('a', { hasText: '查看冻结用例' }).getAttribute('href'), + `suite/cases/${barFile}` + ); + await page.locator('article img').last().scrollIntoViewIfNeeded(); + await page.waitForFunction(() => + [...document.querySelectorAll('article img')].every(image => image.complete && image.naturalWidth > 0) + ); + assert.equal(await page.locator('article img').count(), 3); + await page.locator('#status').selectOption('passed'); + assert.equal(await page.locator('article:visible').count(), 0); + await page.locator('#status').selectOption('diff'); + assert.equal(await page.locator('article:visible').count(), 1); + assert.deepEqual(unexpected, []); + const moved = `${dir}-moved`; + try { + await fs.cp(dir, moved, { recursive: true, filter: source => !source.includes('node_modules') }); + await page.goto(new URL(`file://${moved}/index.html`).href); + await fs.access(path.join(moved, `suite/cases/${barFile}`)); + await page.locator('article img').last().scrollIntoViewIfNeeded(); + await page.waitForFunction(() => [...document.images].every(image => image.complete && image.naturalWidth)); + } finally { + await fs.rm(moved, { recursive: true, force: true }); + } + } finally { + await browser.close(); + } + // 差异图丢失时,报告必须升级为执行错误,并保留诊断。 + const diff = path.join(dir, report.cases[0].images.diff); + await fs.rename(diff, `${diff}.saved`); + const missing = await saveReport(dir, summary()); + assert.equal(missing.status, 'error'); + assert.ok(missing.cases[0].errors.some(error => error.category === 'missing_artifact')); + await fs.rename(`${diff}.saved`, diff); + await fs.writeFile(path.join(dir, 'current.js'), bundle); + }); + await t.test('missing screenshot is an execution error', async () => { + // 缺图不得被自动补成基线。 + const snapshot = path.join(dir, 'snapshots/bar-stack.png'); + await fs.rename(snapshot, `${snapshot}.saved`); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + await assert.rejects(fs.access(snapshot)); + const report = await saveReport(dir, summary()); + assert.equal(report.status, 'error'); + assert.equal(report.cases[0].status, 'error'); + assert.equal(report.cases[0].errors[0].stage, 'comparison'); + await fs.rename(`${snapshot}.saved`, snapshot); + }); + await t.test('missing bundle and runtime exception are execution errors', async () => { + // 同时覆盖资源加载和浏览器运行异常。 + await fs.rm(path.join(dir, 'current.js')); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + await fs.writeFile( + path.join(dir, 'current.js'), + Buffer.concat([bundle, Buffer.from('\nthrow new Error("intentional runtime failure");')]) + ); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + await fs.writeFile(path.join(dir, 'current.js'), bundle); + }); + await t.test('blocked external request is an execution error', async () => { + // 禁止页面请求外网,且失败不能退化成像素差异。 + await fs.writeFile( + path.join(dir, 'current.js'), + Buffer.concat([bundle, Buffer.from('\nfetch("https://example.com/forbidden");')]) + ); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + await fs.writeFile(path.join(dir, 'current.js'), bundle); + }); + await t.test('empty rendering and incorrect static input cannot pass', async () => { + // 即使两份产物都能加载,空绘制和错误图表输入仍是执行错误。 + for (const change of ['data: {id: "data", values: []}', 'type: "line"']) { + await fs.writeFile( + path.join(dir, 'current.js'), + Buffer.concat([ + bundle, + Buffer.from( + `\nconst Original=VChart.default;VChart.default=class extends Original {constructor(spec,options){super({...spec,${change}},options);}};` + ) + ]) + ); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + const result = JSON.parse(await fs.readFile(path.join(dir, 'current-result.json'))); + assert.equal(result.tests[0].status, 'error'); + } + await fs.writeFile(path.join(dir, 'current.js'), bundle); + }); + await t.test('render timeout is an execution error', async () => { + // 只缩短验证副本的超时,避免故障自检等待正式的三十秒。 + const configFile = path.join(dir, 'suite/settings.mjs'); + const config = await fs.readFile(configFile, 'utf8'); + await fs.writeFile(configFile, config.replace('timeout: 30000', 'timeout: 1000')); + const pageFile = path.join(dir, 'suite/page.html'); + const page = await fs.readFile(pageFile, 'utf8'); + await fs.writeFile(pageFile, page.replace('window.__visualReady = true', 'window.__visualReady = false')); + await assert.rejects(executePhase(dir, 'current', url, 'bar-stack')); + await fs.writeFile(configFile, config); + await fs.writeFile(pageFile, page); + }); + await t.test('failed build command rejects instead of using an old artifact', async () => { + // 命令退出非零时立即停止,不能继续读取已有构建文件。 + await assert.rejects(run(process.execPath, ['-e', 'process.exit(7)'], root, path.join(dir, 'failure.log'))); + }); + await t.test('ineffective interactions cannot pass', async () => { + // 分别抑制四类交互,验证状态断言而非只验证图片文件存在。 + const configFile = path.join(dir, 'suite/settings.mjs'); + const config = await fs.readFile(configFile, 'utf8'); + await fs.writeFile(configFile, config.replace('timeout: 30000', 'timeout: 1500')); + for (const id of ['legend-filter', 'tooltip-hover', 'datazoom-drag', 'update-resize']) { + const casesFile = path.join(dir, 'suite/cases', caseMetadata.find(item => item.id === id).file); + const original = await fs.readFile(casesFile, 'utf8'); + assert.ok(original.includes('export default {'), '故障注入必须命中真实模块'); + // 替换模块的动作,保留原始 verify;不依赖某一行鼠标代码的格式。 + await fs.writeFile( + casesFile, + original.replace('export default {', 'const original = {') + + '\nexport default {...original, async exercise() {}};\n' + ); + await assert.rejects(executePhase(dir, 'baseline', url, id), id); + const result = JSON.parse(await fs.readFile(path.join(dir, 'baseline-result.json'), 'utf8')); + assert.equal(result.tests[0].id, id); + assert.equal(result.tests[0].status, 'error'); + await fs.writeFile(casesFile, original); + } + await fs.writeFile(configFile, config); + }); + await t.test('server cannot expose files outside its allowlist', async () => { + // 服务只能读取两份产物和冻结用例。 + assert.equal((await fetch(`${url}/summary.json`)).status, 404); + assert.equal((await fetch(`${url}/suite/%2e%2e%2fcurrent.js`)).status, 404); + }); + } finally { + server.closeAllConnections(); + await new Promise(resolve => server.close(resolve)); + await fs.rm(dir, { recursive: true, force: true }); + } + } +); + +test('preparation errors preserve not-run cases and escape HTML', async () => { + // 未进入 Playwright 的失败也生成可读报告,不伪造图片、通过状态或详细报告链接。 + const dir = await fs.mkdtemp(path.join(root, '.vchart-visual/report-check-')); + try { + const report = await saveReport(dir, { + status: 'error', + stage: 'localBuild', + error: '', + environment: {}, + cases: [{ id: 'bar-stack', purpose: 'minimal' }] + }); + assert.equal(report.complete, false); + assert.equal(report.counts.not_run, 1); + assert.deepEqual(report.logs, {}); + assert.deepEqual(report.cases[0].images, { baseline: null, current: null, diff: null }); + const html = await fs.readFile(path.join(dir, 'index.html'), 'utf8'); + assert.ok(!html.includes('