diff --git a/.github/workflows/statplots-test.yml b/.github/workflows/statplots-test.yml index b6ffbc8a6e..694944c8ad 100644 --- a/.github/workflows/statplots-test.yml +++ b/.github/workflows/statplots-test.yml @@ -4,6 +4,9 @@ on: paths: - 'packages/vchart-statplots/**' - 'packages/vchart/src/**' + - 'packages/vchart-extension/scripts/visual/**' + - 'packages/vchart-extension/__tests__/runtime/visual-build.test.cjs' + - 'common/config/flow-ci.json' - 'common/config/rush/**' - 'rush.json' - '.github/workflows/statplots-test.yml' @@ -14,6 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: '22' @@ -36,9 +42,16 @@ jobs: - run: npm test --prefix packages/vchart-statplots -- --runInBand - name: Skill resource synchronization regression run: node --test packages/vchart-statplots/__tests__/runtime/skill-resources.test.cjs - - run: npm run test:browser --prefix packages/vchart-statplots + - name: Visual suite build regression + run: node --test packages/vchart-extension/__tests__/runtime/visual-build.test.cjs + - run: npm run test:browser --prefix packages/vchart-statplots -- --reporter=json,html,line + env: + PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/.vchart-statplots/statplots-browser.json + PLAYWRIGHT_HTML_OUTPUT_DIR: ${{ github.workspace }}/.vchart-statplots/report - uses: actions/upload-artifact@v4 if: always() with: name: statplots-browser-report - path: .vchart-statplots/report + path: | + .vchart-statplots/report + .vchart-statplots/statplots-browser.json diff --git a/.github/workflows/visactor-trusted-ci.yml b/.github/workflows/visactor-trusted-ci.yml new file mode 100644 index 0000000000..4921a4045e --- /dev/null +++ b/.github/workflows/visactor-trusted-ci.yml @@ -0,0 +1,36 @@ +name: VisActor trusted CI dispatch +run-name: VisActor trusted CI / ${{ inputs.request_digest }} + +# 只受理默认分支的固定控制请求;PR 代码不会在持有 GitHub 凭据的 job 中执行。 +on: + workflow_dispatch: + inputs: + request: + description: Version-bound CI request from the trusted controller + required: true + type: string + request_digest: + description: SHA-256 of the canonical request + required: true + type: string + +permissions: + contents: read + +jobs: + dispatch: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22.22.2' + - name: Confirm the bound request (does not execute tests) + env: + FLOW_CI_REQUEST: ${{ inputs.request }} + FLOW_CI_REQUEST_DIGEST: ${{ inputs.request_digest }} + run: node common/scripts/flow-ci-dispatch.mjs diff --git a/common/changes/@visactor/vchart/upgrade-vutils-vrender_2026-10-16.json b/common/changes/@visactor/vchart/upgrade-vutils-vrender_2026-10-16.json new file mode 100644 index 0000000000..94c3caf6cb --- /dev/null +++ b/common/changes/@visactor/vchart/upgrade-vutils-vrender_2026-10-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "升级 VUtils 家族依赖至 ~1.0.28、VRender 家族依赖至 1.1.12,恢复 LOWESS 历史默认参数并保留算法正确性修复。", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/common/config/flow-ci.json b/common/config/flow-ci.json new file mode 100644 index 0000000000..70af1128aa --- /dev/null +++ b/common/config/flow-ci.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "kind": "trusted_ci_policy", + "repository": "VisActor/VChart", + "baseRef": "develop", + "nodeMajor": 22, + "rushVersion": "5.148.0", + "pnpmVersion": "10.7.0", + "unitPackages": [ + "vchart", + "vchart-extension", + "vchart-statplots", + "vchart-schema", + "vutils-extension" + ], + "buildTargets": [ + "@visactor/vchart", + "@visactor/vchart-extension", + "@visactor/vchart-statplots" + ], + "visualProduct": "vchart", + "visualSelection": "full", + "requiredChecks": [ + "VisActor Flow / Build", + "VisActor Flow / Unit", + "VisActor Flow / Repository visual" + ], + "firefoxPaste": false, + "preBuildScripts": [ + { + "package": "vchart-statplots", + "script": "build:schema" + } + ], + "visualSuites": [ + "core", + "extension" + ], + "browserChecks": [ + { + "package": "vchart-statplots", + "config": "__tests__/browser/playwright.config.ts", + "shards": 2 + } + ], + "buildFormats": [ + "es", + "cjs" + ], + "nodeChecks": [ + "packages/vchart-statplots/__tests__/runtime/skill-resources.test.cjs", + "packages/vchart-extension/__tests__/runtime/visual-build.test.cjs" + ], + "publishedUmd": { + "version": "2.1.8-alpha.0", + "files": { + "vchart": { + "integrity": "sha512-Ufk0lzLHcaJD9BrWcIxEp66UEYtNoiobD4z/dOjJy8qBWqwaclsnx0XZ3wr/S1Da1PQE/zUhy6vLG+uLvq/5yw==", + "sha256": "9d0f5651b1e322f383c7101b54265c9b3a7ee0f5cd7c9165db83f8f19f22cd0b" + }, + "statplots": { + "integrity": "sha512-DgNAwc9LPG2aLW9DD7qR8Px6Oo20HCI4afwliyzRlkWSqKu30DyVL6yJQ9LtyddWMQ9TVPGcSdMIRxPdFUavBA==", + "sha256": "4902756722b23bd9f54b8b4b17ed20ccaba28301cc5f0971e734e1fc20655a04" + } + } + } +} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 9d903193ba..2f3897197a 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -55,8 +55,8 @@ importers: specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 axios: specifier: ^1.4.0 version: 1.19.0 @@ -206,14 +206,14 @@ importers: specifier: workspace:2.1.7 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 react-is: specifier: ^18.2.0 version: 18.3.1 @@ -297,14 +297,14 @@ importers: specifier: workspace:2.1.7 version: link:../vchart-extension '@visactor/vrender-core': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 react-is: specifier: ^18.2.0 version: 18.3.1 @@ -526,32 +526,32 @@ importers: ../../packages/vchart: dependencies: '@visactor/vdataset': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vlayouts': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vrender': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-animate': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-components': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-core': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vscale': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vutils-extension': specifier: workspace:2.1.7 version: link:../vutils-extension @@ -695,42 +695,27 @@ importers: specifier: workspace:2.1.7 version: link:../vchart '@visactor/vdataset': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vlayouts': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vrender-animate': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-components': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-core': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 devDependencies: - '@visactor/vscale': - specifier: ~1.0.27 - version: 1.0.27 - react-device-detect: - specifier: ^2.2.2 - version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - ts-jest: - specifier: ^26.0.0 - version: 26.5.6(jest@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) - jest-electron: - specifier: ^0.1.12 - version: 0.1.12(jest@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5))) - '@types/jest': - specifier: ^26.0.0 - version: 26.0.24 '@internal/bundler': specifier: workspace:* version: link:../../tools/bundler @@ -743,6 +728,9 @@ importers: '@rushstack/eslint-patch': specifier: ~1.1.4 version: 1.1.4 + '@types/jest': + specifier: ^26.0.0 + version: 26.0.24 '@types/node': specifier: 20.14.10 version: 20.14.10 @@ -755,6 +743,9 @@ importers: '@types/react-dom': specifier: ^18.0.0 version: 18.3.7(@types/react@18.3.31) + '@visactor/vscale': + specifier: ~1.0.28 + version: 1.0.28 '@vitejs/plugin-react': specifier: 3.1.0 version: 3.1.0(vite@3.2.6(@types/node@20.14.10)(less@4.1.3)(sass@1.32.11)(stylus@0.54.8)(sugarss@2.0.0)(terser@5.17.1)) @@ -767,9 +758,15 @@ importers: jest: specifier: ^26.0.0 version: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) + jest-electron: + specifier: ^0.1.12 + version: 0.1.12(jest@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5))) react: specifier: ^18.0.0 version: 18.3.1 + react-device-detect: + specifier: ^2.2.2 + version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: ^18.0.0 version: 18.3.1(react@18.3.1) @@ -791,6 +788,9 @@ importers: rollup-plugin-visualizer: specifier: 5.9.2 version: 5.9.2(rollup@3.20.5) + ts-jest: + specifier: ^26.0.0 + version: 26.5.6(jest@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) ts-loader: specifier: 8.0.2 version: 8.0.2(typescript@4.9.5) @@ -846,8 +846,8 @@ importers: ../../packages/vchart-statplots: dependencies: '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 devDependencies: '@internal/bundler': specifier: workspace:* @@ -916,11 +916,11 @@ importers: ../../packages/vutils-extension: dependencies: '@visactor/vdataset': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 devDependencies: '@internal/bundler': specifier: workspace:* @@ -1342,17 +1342,17 @@ importers: specifier: workspace:2.1.7 version: link:../../packages/vchart '@visactor/vrender': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-core': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vrender-kits': - specifier: 1.1.11 - version: 1.1.11 + specifier: 1.1.12 + version: 1.1.12 '@visactor/vutils': - specifier: ~1.0.27 - version: 1.0.27 + specifier: ~1.0.28 + version: 1.0.28 devDependencies: '@internal/bundler': specifier: workspace:* @@ -3151,11 +3151,11 @@ packages: '@visactor/vdataset@0.18.18': resolution: {integrity: sha512-lye23zpineMKV42JmuJaOY3fgl7aWhyDIwK9dWooqZzP14AFukPoK7ZvUeuKZihLrHxqtCg2VWEjovnh9O1RUg==} - '@visactor/vdataset@1.0.27': - resolution: {integrity: sha512-uqdZoc1VPCYh4VGEHKAgBGmI04wIufvs/GTFXQ9KxVQJErX0CnAltGStgjMu3FlO+uH8Wn8/piK+/e/Cs8bwuA==} + '@visactor/vdataset@1.0.28': + resolution: {integrity: sha512-MYSXDKUkNToaheTFiaeV0eyD6Gv3LxiUG2juxuLlpz2WSsTtEbK2qkr3TZ5rynAbHRLwqH0BZUOzl9Th9cMC8Q==} - '@visactor/vlayouts@1.0.27': - resolution: {integrity: sha512-CtWQXhFApHKiy+7xI6WOSb7lc0SecyBsP+MV5yRlyiVOOX7WEjHxPcqec8ask4lBOpAj/S8GClNIF+nBOMGy+A==} + '@visactor/vlayouts@1.0.28': + resolution: {integrity: sha512-GTlL/1N/y5ZY/Yr0KK2d1BGZdhJI2cEziSaNnZPLs4VTVZk7Bb0mG8Nh7qbUggmn7DmXSqjWYsX3rOXLqOefyw==} '@visactor/vmind@1.2.4-alpha.5': resolution: {integrity: sha512-8Z/MFLN0bMLUMofurQ4175AHA5+Yln9CwA6ub1pMzsa+xTjGKALj3D7SseHUi4LSKSB3UGFwckqhKlKynrOfsA==} @@ -3163,35 +3163,35 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.11': - resolution: {integrity: sha512-GBES37lUpM6HupDVSIAJz6YVkOaH2hNFRPR43+ZggE97KDeOCqQk+JXcEsrHjX18zFDXLJ2GIjwXvK+En5n8Jw==} + '@visactor/vrender-animate@1.1.12': + resolution: {integrity: sha512-H7G0x7TEcDSzBCGPAou3BgUqiJ3Oa6y0DbLNtdiUdwb0nLzHoQD/MHKoap/DBsiIXmqTz+XtiL5EuRTFiHq3ZQ==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.11': - resolution: {integrity: sha512-7qpf59Q8NWYZ3SpSjLyri5S2KmoDXqTCu0X4nGgqtV78aPFgmeHxvsonQu4t8uSF/xK0k3VY6GWtuC++Y9ubLw==} + '@visactor/vrender-components@1.1.12': + resolution: {integrity: sha512-ltF7oYPZLpkYDyHfzfmrg0ybgfcRiGi/69x0XnIAhCkeqQ9KCNAzqLgFGOLKJbJFAlpJMolRhkrMO/qq/YGRGw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.11': - resolution: {integrity: sha512-JmSY4d9PX28cFNBkBY1i/vHhNFpSNmqvHCSI6/QIcfrEsyVyk9HOCke6R6PSagpn5dkSo4ZfnECG6/CwlQUyoQ==} + '@visactor/vrender-core@1.1.12': + resolution: {integrity: sha512-LMdMUdSAwg7kH8fQF7+qDc6ZCtoY9zxoguZ78BP0FMEELzXw/1DxDStTMDEzQCA+hlCxYnnRtM4xkkAjKTKSew==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.11': - resolution: {integrity: sha512-ebDopmBSmz0VXPHiozLDP9WOU10miJ3a0vCQnjeyygD8VglWfA+Yx6Zuwr3osEMX7MRNlCCtcbgAPP1s43JuTA==} + '@visactor/vrender-kits@1.1.12': + resolution: {integrity: sha512-AbWaiIyAGsldD30gQjXbZZAvCF+aMpH1PkigEbReOPPoxAcSogv7mV3+3tb22xTqFFnRkK35dwpH+fZ12WyZng==} - '@visactor/vrender@1.1.11': - resolution: {integrity: sha512-MBI/XhUuhp1i0zv2dW/DtLpSwKGrdRNPcX+3UMQu5gPKq26zV0DtPfnI2ELwo+8gQtuiZ+qf6pP7I0g/fUGxoQ==} + '@visactor/vrender@1.1.12': + resolution: {integrity: sha512-ETehbA7UNwZcMdH3kKnzc/3O2APqkBKmReETC+z7QSjAP6xs0+4KhzBW9cwDV9Q4RWkh2pRFh/MJp1aea+PH7g==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} - '@visactor/vscale@1.0.27': - resolution: {integrity: sha512-0qtlegsx5EljBJbig2g4Pm//y89NYS44JhZfjeLQlPpLgRmH7K0yMaL2u+s6lwvetmCTdwfJYxX10RHSOxYzDw==} + '@visactor/vscale@1.0.28': + resolution: {integrity: sha512-VT0mQ1L5gE6cpeaJKVmy8+RdOpVcT0wSRsbDyV1aK026Jt3cTm656A+tcY/iKCbQZu/mYw2d7wWCPYs708pAtA==} '@visactor/vscale@1.0.4': resolution: {integrity: sha512-mXuX0gbQ5dmsU+dOfrDfFT45ijTZrFh1wYeIY44cdMhFo4v+tVdeihN0F+3CEI7oSZiZENbpJ7dXvxnu04xG/g==} @@ -3220,8 +3220,8 @@ packages: '@visactor/vutils@0.19.7': resolution: {integrity: sha512-1SSnkZgX1p/rSVIFEibrpN6rDdLfdETSI6lJI5JwV8I2paluM1mqz3jEeT3McmWygd/wyUVKAyoRxGXFKAsKEw==} - '@visactor/vutils@1.0.27': - resolution: {integrity: sha512-nHwOtZSL5l5yTbs08FV/+gFPkpVSdZVZyDJ6Ay6EU1NhvcPrGuyeB8TTUXBxo/77OC0CzEdCmOxgmzGl4KmUjQ==} + '@visactor/vutils@1.0.28': + resolution: {integrity: sha512-pUPc/qd3sIGeLKXzeyHFOouVmlWWqKS6M05zczk1RqvOnuiHtP1P2iHV6pqI2X5uxfuutq2LvXnrO8VwgPVpKg==} '@visactor/vutils@1.0.4': resolution: {integrity: sha512-GE149SM5WAc9DMNV7bGtPD4xHP68vbHMRuxGPJ3ndzAGLC/KuXpClteMw6bTY1fRX1vDLY/tQ/GVthgeOx4kDw==} @@ -13713,7 +13713,7 @@ snapshots: jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 - jest-runner: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) + jest-runner: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)) jest-runtime: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) jest-snapshot: 26.6.2 jest-util: 26.6.2 @@ -13847,6 +13847,20 @@ snapshots: - ts-node - utf-8-validate + '@jest/test-sequencer@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5))': + dependencies: + '@jest/test-result': 26.6.2 + graceful-fs: 4.2.11 + jest-haste-map: 26.6.2 + jest-runner: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)) + jest-runtime: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/transform@24.9.0': dependencies: '@babel/core': 7.20.12 @@ -15100,12 +15114,12 @@ snapshots: simplify-geojson: 1.0.5 topojson-client: 3.1.0 - '@visactor/vdataset@1.0.27': + '@visactor/vdataset@1.0.28': dependencies: '@turf/flatten': 6.5.0 '@turf/helpers': 6.5.0 '@turf/rewind': 6.5.0 - '@visactor/vutils': 1.0.27 + '@visactor/vutils': 1.0.28 d3-dsv: 2.0.0 d3-geo: 1.12.1 d3-hexbin: 0.2.2 @@ -15120,12 +15134,12 @@ snapshots: simplify-geojson: 1.0.5 topojson-client: 3.1.0 - '@visactor/vlayouts@1.0.27': + '@visactor/vlayouts@1.0.28': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 - '@visactor/vscale': 1.0.27 - '@visactor/vutils': 1.0.27 + '@visactor/vscale': 1.0.28 + '@visactor/vutils': 1.0.28 eventemitter3: 4.0.7 '@visactor/vmind@1.2.4-alpha.5': @@ -15150,10 +15164,10 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.11': + '@visactor/vrender-animate@1.1.12': dependencies: - '@visactor/vrender-core': 1.1.11 - '@visactor/vutils': 1.0.27 + '@visactor/vrender-core': 1.1.12 + '@visactor/vutils': 1.0.28 '@visactor/vrender-components@1.0.0-alpha.18': dependencies: @@ -15163,22 +15177,22 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.11': + '@visactor/vrender-components@1.1.12': dependencies: - '@visactor/vrender-animate': 1.1.11 - '@visactor/vrender-core': 1.1.11 - '@visactor/vrender-kits': 1.1.11 - '@visactor/vscale': 1.0.27 - '@visactor/vutils': 1.0.27 + '@visactor/vrender-animate': 1.1.12 + '@visactor/vrender-core': 1.1.12 + '@visactor/vrender-kits': 1.1.12 + '@visactor/vscale': 1.0.28 + '@visactor/vutils': 1.0.28 '@visactor/vrender-core@1.0.0-alpha.18': dependencies: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.11': + '@visactor/vrender-core@1.1.12': dependencies: - '@visactor/vutils': 1.0.27 + '@visactor/vutils': 1.0.28 color-convert: 2.0.1 '@visactor/vrender-kits@1.0.0-alpha.18': @@ -15190,29 +15204,29 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.11': + '@visactor/vrender-kits@1.1.12': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.11 - '@visactor/vutils': 1.0.27 + '@visactor/vrender-core': 1.1.12 + '@visactor/vutils': 1.0.28 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.11': + '@visactor/vrender@1.1.12': dependencies: - '@visactor/vrender-animate': 1.1.11 - '@visactor/vrender-components': 1.1.11 - '@visactor/vrender-core': 1.1.11 - '@visactor/vrender-kits': 1.1.11 + '@visactor/vrender-animate': 1.1.12 + '@visactor/vrender-components': 1.1.12 + '@visactor/vrender-core': 1.1.12 + '@visactor/vrender-kits': 1.1.12 '@visactor/vscale@0.18.18': dependencies: '@visactor/vutils': 0.18.18 - '@visactor/vscale@1.0.27': + '@visactor/vscale@1.0.28': dependencies: - '@visactor/vutils': 1.0.27 + '@visactor/vutils': 1.0.28 '@visactor/vscale@1.0.4': dependencies: @@ -15273,7 +15287,7 @@ snapshots: '@turf/invariant': 6.5.0 eventemitter3: 4.0.7 - '@visactor/vutils@1.0.27': + '@visactor/vutils@1.0.28': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 @@ -21025,7 +21039,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2(encoding@0.1.13)) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -21043,7 +21057,7 @@ snapshots: jest-config@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)): dependencies: '@babel/core': 7.20.12 - '@jest/test-sequencer': 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) + '@jest/test-sequencer': 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)) '@jest/types': 26.6.2 babel-jest: 26.6.3(@babel/core@7.20.12) chalk: 4.1.2 @@ -21053,7 +21067,7 @@ snapshots: jest-environment-jsdom: 26.6.2(canvas@2.11.2(encoding@0.1.13)) jest-environment-node: 26.6.2 jest-get-type: 26.3.0 - jest-jasmine2: 26.6.3 + jest-jasmine2: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) jest-regex-util: 26.0.0 jest-resolve: 26.6.2 jest-util: 26.6.2 @@ -21242,7 +21256,7 @@ snapshots: transitivePeerDependencies: - supports-color - jest-jasmine2@26.6.3: + jest-jasmine2@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)): dependencies: '@babel/traverse': 7.29.8 '@jest/environment': 26.6.2 @@ -21263,7 +21277,11 @@ snapshots: pretty-format: 26.6.2 throat: 5.0.0 transitivePeerDependencies: + - bufferutil + - canvas - supports-color + - ts-node + - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -21411,6 +21429,35 @@ snapshots: - ts-node - utf-8-validate + jest-runner@26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)): + dependencies: + '@jest/console': 26.6.2 + '@jest/environment': 26.6.2 + '@jest/test-result': 26.6.2 + '@jest/types': 26.6.2 + '@types/node': 20.14.10 + chalk: 4.1.2 + emittery: 0.7.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@5.4.5)) + jest-docblock: 26.0.0 + jest-haste-map: 26.6.2 + jest-leak-detector: 26.6.2 + jest-message-util: 26.6.2 + jest-resolve: 26.6.2 + jest-runtime: 26.6.3(canvas@2.11.2(encoding@0.1.13))(ts-node@10.9.0(@types/node@20.14.10)(typescript@4.9.5)) + jest-util: 26.6.2 + jest-worker: 26.6.2 + source-map-support: 0.5.21 + throat: 5.0.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-runtime@24.9.0: dependencies: '@jest/console': 24.9.0 diff --git a/common/scripts/flow-ci-dispatch.mjs b/common/scripts/flow-ci-dispatch.mjs new file mode 100644 index 0000000000..123f540773 --- /dev/null +++ b/common/scripts/flow-ci-dispatch.mjs @@ -0,0 +1,35 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +/** 固定键顺序;派发摘要绑定完整输入,不依赖作者提供的展示标题。 */ +export function canonical(value) { + if (Array.isArray(value)) return value.map(canonical); + if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])); + return value; +} + +/** 默认分支只确认受理身份;候选源码、构建及测试由无凭据隔离侧执行。 */ +export function dispatchReceipt(text, expectedDigest, policy, env) { + if (typeof text !== 'string' || Buffer.byteLength(text) > 4096) throw new Error('派发输入超限'); + const request = JSON.parse(text); + const keys = ['schemaVersion', 'kind', 'requestId', 'repository', 'pullRequest', 'headSha', 'baseSha', 'controlCommit', 'policyDigest', 'dependencyDigest']; + const sha = value => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value); + const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); + if (!request || Array.isArray(request) || Object.keys(request).sort().join() !== keys.sort().join() || + request.schemaVersion !== 1 || request.kind !== 'ci_request' || !hash(request.requestId) || + request.repository !== policy.repository || !Number.isSafeInteger(request.pullRequest) || request.pullRequest < 1 || + ![request.headSha, request.baseSha, request.controlCommit].every(sha) || ![request.policyDigest, request.dependencyDigest, expectedDigest].every(hash)) throw new Error('派发身份非法'); + const requestDigest = createHash('sha256').update(JSON.stringify(canonical(request))).digest('hex'); + if (requestDigest !== expectedDigest || env.GITHUB_EVENT_NAME !== 'workflow_dispatch' || + env.GITHUB_REPOSITORY !== policy.repository || env.GITHUB_REF !== 'refs/heads/' + policy.baseRef || + env.GITHUB_SHA !== request.controlCommit || env.GITHUB_RUN_ATTEMPT !== '1' || !/^[1-9][0-9]*$/.test(env.GITHUB_RUN_ID ?? '')) throw new Error('派发不是当前可信默认分支的首次运行'); + return { schemaVersion: 1, kind: 'ci_dispatch_receipt', requestDigest, request, workflowRunId: env.GITHUB_RUN_ID, + controlCommit: env.GITHUB_SHA, testsExecuted: false }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const policy = JSON.parse(await fs.readFile(new URL('../config/flow-ci.json', import.meta.url), 'utf8')); + console.log(JSON.stringify(dispatchReceipt(process.env.FLOW_CI_REQUEST, process.env.FLOW_CI_REQUEST_DIGEST, policy, process.env))); +} diff --git a/common/scripts/flow-ci-dispatch.test.mjs b/common/scripts/flow-ci-dispatch.test.mjs new file mode 100644 index 0000000000..64a39d56bc --- /dev/null +++ b/common/scripts/flow-ci-dispatch.test.mjs @@ -0,0 +1,46 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import fs from 'node:fs/promises'; +import { canonical, dispatchReceipt } from './flow-ci-dispatch.mjs'; + +const policy = JSON.parse(await fs.readFile(new URL('../config/flow-ci.json', import.meta.url), 'utf8')); +const request = { schemaVersion: 1, kind: 'ci_request', requestId: 'a'.repeat(64), repository: policy.repository, pullRequest: 1, + headSha: 'b'.repeat(40), baseSha: 'c'.repeat(40), controlCommit: 'd'.repeat(40), policyDigest: 'e'.repeat(64), dependencyDigest: 'f'.repeat(64) }; +const hash = value => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex'); +const env = { GITHUB_EVENT_NAME: 'workflow_dispatch', GITHUB_REPOSITORY: policy.repository, GITHUB_REF: 'refs/heads/develop', + GITHUB_SHA: request.controlCommit, GITHUB_RUN_ATTEMPT: '1', GITHUB_RUN_ID: '123' }; + +test('仅受理固定默认分支和精确输入,不宣称执行了测试', () => { + const result = dispatchReceipt(JSON.stringify(request), hash(request), policy, env); + assert.equal(result.testsExecuted, false); assert.equal(result.requestDigest, hash(request)); + assert.equal(result.workflowRunId, '123'); assert.deepEqual(result.request, request); +}); + +for (const change of [{ GITHUB_EVENT_NAME: 'pull_request_target' }, { GITHUB_EVENT_NAME: 'pull_request' }, + { GITHUB_REF: 'refs/pull/1/merge' }, { GITHUB_REF: 'refs/heads/untrusted' }, { GITHUB_REPOSITORY: 'fork/repository' }, + { GITHUB_SHA: '0'.repeat(40) }, { GITHUB_RUN_ATTEMPT: '2' }, { GITHUB_RUN_ID: '0' }]) test('拒绝不可信运行上下文 ' + JSON.stringify(change), () => { + assert.throws(() => dispatchReceipt(JSON.stringify(request), hash(request), policy, { ...env, ...change })); +}); + +for (const change of [{ pullRequest: 0 }, { repository: 'fork/repository' }, { headSha: 'main' }, { extra: true }, + { policyDigest: null }, { dependencyDigest: 'x' }, { kind: 'test_result' }]) test('拒绝未绑定输入 ' + JSON.stringify(change), () => { + const value = { ...request, ...change }; assert.throws(() => dispatchReceipt(JSON.stringify(value), hash(value), policy, env)); +}); + +test('摘要变化和超限输入不能复用受理记录', () => { + assert.throws(() => dispatchReceipt(JSON.stringify({ ...request, headSha: '0'.repeat(40) }), hash(request), policy, env)); + assert.throws(() => dispatchReceipt(' '.repeat(4097), hash(request), policy, env)); +}); + + +test('固定单测清单覆盖当前所有产品包,不因默认分支新增包而漏测', async () => { + const root = new URL('../../packages/', import.meta.url); const expected = []; + for (const entry of await fs.readdir(root, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const name = entry.name; + try { const pkg = JSON.parse(await fs.readFile(new URL(name + '/package.json', root), 'utf8')); if (pkg.scripts?.test) expected.push(name); } + catch (error) { if (error.code !== 'ENOENT') throw error; } + } + assert.deepEqual([...policy.unitPackages].sort(), expected.sort()); +}); diff --git a/docs/package.json b/docs/package.json index 85d3ec7095..8180bb18c5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,9 +18,9 @@ "@visactor/vchart-extension": "workspace:2.1.7", "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", - "@visactor/vutils": "~1.0.27", - "@visactor/vrender": "1.1.11", - "@visactor/vrender-kits": "1.1.11", + "@visactor/vutils": "~1.0.28", + "@visactor/vrender": "1.1.12", + "@visactor/vrender-kits": "1.1.12", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index d0ccc00750..1ff2ff4b9b 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,9 +30,9 @@ "dependencies": { "@visactor/vchart": "workspace:2.1.7", "@visactor/vchart-extension": "workspace:2.1.7", - "@visactor/vutils": "~1.0.27", - "@visactor/vrender-core": "1.1.11", - "@visactor/vrender-kits": "1.1.11", + "@visactor/vutils": "~1.0.28", + "@visactor/vrender-core": "1.1.12", + "@visactor/vrender-kits": "1.1.12", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index c9490e6689..40ed16e515 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -35,9 +35,9 @@ "dependencies": { "@visactor/vchart": "workspace:2.1.7", "@visactor/vchart-extension": "workspace:2.1.7", - "@visactor/vutils": "~1.0.27", - "@visactor/vrender-core": "1.1.11", - "@visactor/vrender-kits": "1.1.11", + "@visactor/vutils": "~1.0.28", + "@visactor/vrender-core": "1.1.12", + "@visactor/vrender-kits": "1.1.12", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/packages/vchart-extension/__tests__/runtime/visual-build.test.cjs b/packages/vchart-extension/__tests__/runtime/visual-build.test.cjs new file mode 100644 index 0000000000..9bb387dab3 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/visual-build.test.cjs @@ -0,0 +1,37 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs/promises'); +const path = require('node:path'); +const os = require('node:os'); + +test('extension export reuses the explicit current core bundle and builds its own resource', async t => { + const { build } = await import('../../scripts/visual/build.mjs'); + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'vchart-extension-core-')); + t.after(() => fs.rm(root, { recursive: true, force: true })); + await fs.mkdir(path.join(root, 'packages/vchart'), { recursive: true }); + await fs.writeFile(path.join(root, 'packages/vchart/tsconfig.json'), JSON.stringify({ compilerOptions: {} })); + const core = path.join(root, 'current-core.js'), output = path.join(root, 'candidate.js'); + await fs.writeFile(core, 'globalThis.currentCore = true;'); + const calls = []; + const runtime = { + async run(command, args, cwd) { + // 真实核心构建会调用其他包;本轮只允许构建扩展,组合内容必须来自给定核心。 + calls.push({ command, args, cwd }); + assert.equal(cwd, path.join(root, 'packages/vchart-extension')); + const directory = path.join(root, 'candidate-extension/build'); + await fs.mkdir(directory, { recursive: true }); + await fs.writeFile(path.join(directory, 'index.js'), 'globalThis.currentExtension = true;'); + } + }; + await build(root, output, runtime, 'build.log', core); + assert.equal(await fs.readFile(core, 'utf8'), 'globalThis.currentCore = true;'); + assert.equal( + await fs.readFile(output, 'utf8'), + 'globalThis.currentCore = true;\n;\nglobalThis.currentExtension = true;' + ); + assert.equal(calls.length, 1); + await assert.rejects(build(root, path.join(root, 'missing.js'), runtime, 'build.log', path.join(root, 'absent.js')), { + code: 'ENOENT' + }); + assert.equal(calls.length, 1); +}); diff --git a/packages/vchart-extension/__tests__/visual/cases/index.mjs b/packages/vchart-extension/__tests__/visual/cases/index.mjs index 0e9b5744cc..64b0f6616b 100644 --- a/packages/vchart-extension/__tests__/visual/cases/index.mjs +++ b/packages/vchart-extension/__tests__/visual/cases/index.mjs @@ -16,6 +16,7 @@ export const cases = [ } ]; +/** 从冻结清单所在目录加载用例,与核心套件共用同一加载契约。 */ export async function loadCase(item) { - return (await import(item.file)).default; + return (await import(new URL(item.file, import.meta.url))).default; } diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 8a226907ef..d55b4a3fe7 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -28,14 +28,14 @@ "test:visual": "node scripts/visual-test.mjs" }, "dependencies": { - "@visactor/vrender-core": "1.1.11", - "@visactor/vrender-kits": "1.1.11", - "@visactor/vrender-components": "1.1.11", - "@visactor/vrender-animate": "1.1.11", + "@visactor/vrender-core": "1.1.12", + "@visactor/vrender-kits": "1.1.12", + "@visactor/vrender-components": "1.1.12", + "@visactor/vrender-animate": "1.1.12", "@visactor/vchart": "workspace:2.1.7", - "@visactor/vutils": "~1.0.27", - "@visactor/vdataset": "~1.0.27", - "@visactor/vlayouts": "~1.0.27" + "@visactor/vutils": "~1.0.28", + "@visactor/vdataset": "~1.0.28", + "@visactor/vlayouts": "~1.0.28" }, "devDependencies": { "@internal/bundler": "workspace:*", @@ -66,7 +66,7 @@ "jest-electron": "^0.1.12", "ts-jest": "^26.0.0", "react-device-detect": "^2.2.2", - "@visactor/vscale": "~1.0.27" + "@visactor/vscale": "~1.0.28" }, "publishConfig": { "access": "public", diff --git a/packages/vchart-extension/scripts/visual/build.mjs b/packages/vchart-extension/scripts/visual/build.mjs index b98d7c497b..e7a6fd7937 100644 --- a/packages/vchart-extension/scripts/visual/build.mjs +++ b/packages/vchart-extension/scripts/visual/build.mjs @@ -4,8 +4,10 @@ import { build as buildCore } from '../../../vchart/scripts/visual/build.mjs'; const buildEnv = { NODE_ENV: 'production', BUNDLE_ANALYZE: '', IGNORE_ENTRIES: 'true' }; -export async function build(root, destination, runtime, log) { - await buildCore(root, destination, runtime, log); +export async function build(root, destination, runtime, log, coreBundle) { + // CI 可复用本轮已冻结的同侧核心资源,避免扩展导出重复编译核心。 + if (coreBundle) await fs.copyFile(coreBundle, destination); + else await buildCore(root, destination, runtime, log); // 使用同一侧源码构建时序扩展;VChart 保持外部依赖,共享 Factory 和主题注册表。 // 基线迁移前由 sequenceAnalysis 注册核心系列,迁移后注册扩展系列。 const extensionDirectory = path.join(path.dirname(destination), `${path.basename(destination, '.js')}-extension`); diff --git a/packages/vchart-statplots/README.md b/packages/vchart-statplots/README.md index 580bd055cb..80c6fb6613 100644 --- a/packages/vchart-statplots/README.md +++ b/packages/vchart-statplots/README.md @@ -85,10 +85,12 @@ npm run eslint --prefix packages/vchart-statplots ## 统计工具依赖 -本包使用 `@visactor/vutils ~1.0.27` 根入口的公共 `normalQuantile`、`mean`、`correlation` 和 `bootstrapEstimate`。这些 API 自 npm `1.0.25` 起提供;仓库依赖和 Rush 锁文件已同步。执行标准 Rush 安装即可使用。 +本包使用 `@visactor/vutils ~1.0.28` 根入口的公共 `normalQuantile`、`mean`、`correlation` 和 `bootstrapEstimate`。这些 API 自 npm `1.0.25` 起提供;仓库依赖和 Rush 锁文件已同步。执行标准 Rush 安装即可使用。 已发布依赖的隔离安装、类型、构建与回归结果见[验收记录](docs/validation.md)。这项升级解决统计工具的依赖阻断;VChart 核心改动及其他发布要求仍须按验收记录核对。 -散点/残差图的内部回归使用 vutils 自 1.0.26 起提供的稳定回归与成对 bootstrap;当前依赖下限为 `~1.0.27`。正式包验证与发布验收状态见[验收记录](docs/validation.md)。 +散点/残差图的内部回归使用 vutils 自 1.0.26 起提供的稳定回归与成对 bootstrap;当前依赖下限为 `~1.0.28`。正式包验证与发布验收状态见[验收记录](docs/validation.md)。 + +VUtils 1.0.28 将 LOWESS 公共默认参数恢复为 `span=0.3`、`iterations=2`,并保留算法正确性修复。StatPlots 继续显式传入 `span=2/3`、`iterations=3`、`delta=0`;业务默认行为保持不变,用户仍可显式覆盖。 KDE/violin 默认对齐 Seaborn 的样本标准差带宽;KDE 默认全体样本归一化,violin 默认等面积、同系列跨分类归一化。旧默认可显式配置,见对应逐图文档。此项使用 npm VUtil 1.0.27 提供的 standardDeviation/bandwidthAdjust 公共接口;相关依赖和 Rush 锁文件已同步。 diff --git a/packages/vchart-statplots/__tests__/browser/hexbin.spec.ts b/packages/vchart-statplots/__tests__/browser/hexbin.spec.ts index 439e194e3a..8642928e98 100644 --- a/packages/vchart-statplots/__tests__/browser/hexbin.spec.ts +++ b/packages/vchart-statplots/__tests__/browser/hexbin.spec.ts @@ -81,31 +81,55 @@ async function read(page: Page) { }); } type Snapshot = Awaited>; +// 逐箱校验相同的几何约束,只汇总失败,避免大数据用例产生数万次测试框架调用。 function expectRegular(result: Snapshot, count: number) { expect(result.errors).toEqual([]); expect(result.cells.reduce((sum: number, cell: Snapshot['cells'][number]) => sum + cell.datum.count, 0)).toBe(count); let maxDeviation = 0; - for (const cell of result.cells) { - expect(cell.datum.count).toBeGreaterThan(0); - expect(cell.points).toHaveLength(6); + const failures: string[] = []; + for (const [index, cell] of result.cells.entries()) { + if (!(cell.datum.count > 0)) failures.push(`cell ${index}: count ${cell.datum.count}`); + if (cell.points.length !== 6) { + failures.push(`cell ${index}: ${cell.points.length} vertices`); + continue; + } const edges = cell.points.map((p, i) => { const next = cell.points[(i + 1) % 6]; return Math.hypot(p.x - next.x, p.y - next.y); }); maxDeviation = Math.max(maxDeviation, Math.max(...edges) - Math.min(...edges)); - expect(Math.min(...edges)).toBeGreaterThan(0); + if (!(Math.min(...edges) > 0)) failures.push(`cell ${index}: edge lengths ${edges}`); cell.points.forEach((p, i) => { const prev = cell.points[(i + 5) % 6], next = cell.points[(i + 1) % 6]; const cosine = ((prev.x - p.x) * (next.x - p.x) + (prev.y - p.y) * (next.y - p.y)) / (edges[i] * edges[(i + 5) % 6]); - expect(Math.abs(cosine + 0.5)).toBeLessThan(1e-7); + if (!(Math.abs(cosine + 0.5) < 1e-7)) failures.push(`cell ${index}, vertex ${i}: cosine ${cosine}`); }); } + expect(failures).toEqual([]); expect(maxDeviation).toBeLessThan(1e-5); } const raw = (snapshot: Snapshot) => snapshot.cells.map(cell => cell.datum); +test('regularity checks reject missing, collapsed, skewed and non-finite vertices', () => { + const points = Array.from({ length: 6 }, (_, i) => ({ + x: Math.cos((i * Math.PI) / 3), + y: Math.sin((i * Math.PI) / 3) + })); + const snapshot = (vertices: typeof points): Snapshot => + ({ cells: [{ points: vertices, datum: { count: 1 } }], errors: [], region: { width: 2, height: 2 } } as Snapshot); + expectRegular(snapshot(points), 1); + for (const vertices of [ + points.slice(1), + points.map(() => ({ x: 0, y: 0 })), + points.map((point, i) => (i ? point : { ...point, x: point.x + 0.1 })), + points.map((point, i) => (i ? point : { ...point, x: NaN })) + ]) { + expect(() => expectRegular(snapshot(vertices), 1)).toThrow(); + } +}); + test('raw scatter renders regular neighboring hexagons and retains the same bins across resize and themes', async ({ page }) => { diff --git a/packages/vchart-statplots/docs/computation-contract.md b/packages/vchart-statplots/docs/computation-contract.md index 34e4846f27..a0980dc8d5 100644 --- a/packages/vchart-statplots/docs/computation-contract.md +++ b/packages/vchart-statplots/docs/computation-contract.md @@ -62,6 +62,6 @@ missingValueMatrix 与 correlationMatrix 从 variables/data.observations 生成 一元线性、多项式和 LOWESS 属于图内统计,在 compile 阶段按系列独立执行。scatterPlot 内部 fit 从同一 data 原始 x/y 拟合;外部 estimates 只展示。residualPlot 原始模式直接在观测 x 上预测,regression 控制生成残差的模型,smooth 单独控制趋势。外部预测和标准化/学生化模型残差继续保留。两图共用 vutils 标准求解路径与私有分组适配,不把模型对象存入图元数据,不在交互阶段重算。 -线性/多项式均值响应置信带为成对 percentile bootstrap;LOWESS 无内部置信带。全量 LOWESS 的 delta 是显式插值策略,不丢观测或自动关闭稳健迭代。使用 vutils 自 1.0.26 起提供的稳定回归和 regressionBootstrap,正式依赖下限为 `~1.0.27`。 +线性/多项式均值响应置信带为成对 percentile bootstrap;LOWESS 无内部置信带。全量 LOWESS 的 delta 是显式插值策略,不丢观测或自动关闭稳健迭代。使用 vutils 自 1.0.26 起提供的稳定回归和 regressionBootstrap,正式依赖下限为 `~1.0.28`。 时间序列 input=observations 按系列及真实时刻汇总,默认均值与 95% bootstrap CI;input=estimates 展示外部区间。日期稳定排序,同一真实时刻的时区别名在原始模式合并、外部模式拒绝重复。null 保留显式缺口,不补时序网格。算法与 pointIntervalPlot 共用,详见[时间序列](plots/timeSeriesPlot.md)。 diff --git a/packages/vchart-statplots/docs/plots/scatterPlot.md b/packages/vchart-statplots/docs/plots/scatterPlot.md index 8b237f9485..0566d7aa09 100644 --- a/packages/vchart-statplots/docs/plots/scatterPlot.md +++ b/packages/vchart-statplots/docs/plots/scatterPlot.md @@ -42,6 +42,6 @@ LOWESS 无内部置信带,只允许省略 confidence 或 false。同样本同 纯散点对应 seaborn.scatterplot;内部拟合对应 [seaborn.regplot](https://seaborn.pydata.org/generated/seaborn.regplot.html) 的原始输入、模型与区间含义。LOWESS 参数含义参考 [statsmodels.lowess](https://www.statsmodels.org/stable/generated/statsmodels.nonparametric.smoothers_lowess.lowess.html)。本包严格数值边界、小样本 CI 规则、固定 RNG、退化 bootstrap 基底与 Python 有明确差异,不宣称全参数或逐位相同。 -内部回归使用 vutils 自 1.0.26 起提供的稳定求解、LOWESS 和 regressionBootstrap,当前依赖下限为 `~1.0.27`。正式包隔离安装与验收状态见[验收记录](../validation.md)。 +内部回归使用 vutils 自 1.0.26 起提供的稳定求解、LOWESS 和 regressionBootstrap,当前依赖下限为 `~1.0.28`。正式包隔离安装与验收状态见[验收记录](../validation.md)。 [最小纯散点](../../examples/specs/minimal/scatterPlot.json);[主例](../../examples/specs/scatterPlot.json);[Schema](../../schema/plots/scatterPlot.schema.json)。 diff --git a/packages/vchart-statplots/package.json b/packages/vchart-statplots/package.json index 72782daae1..5739442789 100644 --- a/packages/vchart-statplots/package.json +++ b/packages/vchart-statplots/package.json @@ -38,7 +38,7 @@ "@visactor/vchart": "2.1.7" }, "dependencies": { - "@visactor/vutils": "~1.0.27" + "@visactor/vutils": "~1.0.28" }, "devDependencies": { "@visactor/vchart": "workspace:2.1.7", diff --git a/packages/vchart/__tests__/unit/component/brush-lifecycle.test.ts b/packages/vchart/__tests__/unit/component/brush-lifecycle.test.ts index e246c41012..4b5177e3e9 100644 --- a/packages/vchart/__tests__/unit/component/brush-lifecycle.test.ts +++ b/packages/vchart/__tests__/unit/component/brush-lifecycle.test.ts @@ -1,4 +1,53 @@ import { Brush } from '../../../src/component/brush/brush'; + +test.each([1, 4, 64])('%i regions reset ten thousand graphic states in one layout pass', regions => { + const brush = Object.create(Brush.prototype) as any; + let stateUpdates = 0; + // 用实际状态迁移计数约束工作量,不能为每个面板重复遍历整张图。 + const graphics = Array.from({ length: 10000 }, (_, i) => ({ + context: { key: String(i) }, + currentStates: ['hover', i % 2 ? 'inBrush' : 'outOfBrush'], + setStates(states: string[]) { + this.currentStates = states; + stateUpdates++; + } + })); + brush._option = {}; + brush._spec = { visible: true }; + brush.getMarks = (): unknown[] => []; + brush._relativeRegions = Array.from({ length: regions }, (_, id) => ({ id })); + brush._brushComponents = brush._relativeRegions.map(() => ({ + setAttributes: jest.fn(), + children: [{ removeAllChild: jest.fn() }] + })); + brush._getBrushInteractiveAttr = () => ({ interactiveRange: { x1: 0, x2: 100, y1: 0, y2: 100 } }); + const marks = brush._relativeRegions.map((_: unknown, index: number) => ({ + id: index, + getGraphics: () => + graphics.slice( + Math.floor((index * graphics.length) / regions), + Math.floor(((index + 1) * graphics.length) / regions) + ) + })); + brush._itemMap = Object.fromEntries(marks.map((mark: unknown, index: number) => [index, [mark]])); + brush._linkedItemMap = brush._itemMap; + + brush.onLayoutEnd(); + + expect(stateUpdates).toBe(2 * graphics.length); + expect(graphics.every(graphic => graphic.currentStates.length === 1 && graphic.currentStates[0] === 'hover')).toBe( + true + ); + expect(Object.keys(brush._inBrushElementsMap)).toHaveLength(0); + expect(Object.keys(brush._linkedInBrushElementsMap)).toHaveLength(0); + expect(Object.keys(brush._outOfBrushElementsMap)).toHaveLength(graphics.length); + expect(Object.keys(brush._linkedOutOfBrushElementsMap)).toHaveLength(graphics.length); + brush._brushComponents.forEach((component: any) => { + expect(component.setAttributes).toHaveBeenCalledTimes(1); + expect(component.children[0].removeAllChild).toHaveBeenCalledTimes(1); + }); +}); + test('clears all brush state before releasing any region container', () => { const brush = Object.create(Brush.prototype) as any; const children = [0, 1, 2].map(() => ({ removeAllChild: jest.fn() })); diff --git a/packages/vchart/__tests__/visual/README.md b/packages/vchart/__tests__/visual/README.md index 143b05f3f2..9251b255bb 100644 --- a/packages/vchart/__tests__/visual/README.md +++ b/packages/vchart/__tests__/visual/README.md @@ -55,6 +55,72 @@ node packages/vchart/scripts/visual-test.mjs --check 退出码:`0` 无差异;`1` 有视觉差异;`2` 参数、构建、执行、资源、超时、缺图或清理错误。多个问题同时出现时执行错误优先。 +## 冻结套件导出 + +平台接入和归档可以复用同一个命令导出完整用例与当前构建,不需要 BugServer 登录: + +```sh +node packages/vchart/scripts/visual-test.mjs --export-suite .vchart-visual/suites/current +node packages/vchart/scripts/visual-test.mjs --export-suite .vchart-visual/suites/next --previous-suite .vchart-visual/suites/current/suite-manifest.json +``` + +导出目录必须是新目录,位于工作区外或 Git 忽略目录;已有目录不会被覆盖。此模式不能与单例、目录、基线、列举或环境预检选项混用。它使用既有构建链生成新的 `current.js`,冻结原格式用例、页面、Playwright 运行器和辅助文件,并保存包版本与依赖锁文件;不下载官方基线、不运行截图、不接受视觉差异。 + +`suite-manifest.json` 使用 `schemaVersion=1`、`contractVersion=1.0`、`kind=suite_manifest`。所有文件记录相对路径、字节大小和 SHA-256;`suiteDigest` 标识规范化完整清单,`filesDigest` 标识冻结文件集合,`bundleDigest` 标识实际产品资源。`caseDigest` 包含用例、相对导入、公共运行器和使用的资源,截图键固定为 `final`,对应现有单例最终截图。历史 BugServer IDs 仅记录来源,不用于覆盖 Photo 用例,也不会写回源码。命令的 JSON 输出包含清单位置及按稳定 case ID 识别的 add/update/archive;归档只表示清单差异,不删除平台数据。 + +导出时校验唯一 ID/模块、完整资源、导入路径、符号链接及构建期工作区变化;失败退出 `2`,回收本次子进程,保留目录中的日志和 `export-failure.json`,移除不可消费的成功清单。重新导出使用新目录。套件列举和导入会执行 JavaScript:平台须把导出/发现放入隔离 worker,不能在有内网或业务凭据的控制进程执行。清单中的环境要求固定 Node 22、实际 Playwright 版本和现有截图设置;完整镜像、浏览器、字体和网络隔离仍由平台环境验收负责,导出成功不表示视觉测试或隔离验收通过。 + +确定性和非法输入检查: + +```sh +node --test packages/vchart/scripts/visual-suite.test.mjs +``` + +## 冻结候选与指定只读基准 + +同一用例还可以验证冻结构建并与平台提供的 PNG 集合比较,不重新构建或采集基准: + +```sh +node packages/vchart/scripts/visual-test.mjs --run-suite .vchart-visual/suites/current --run-request /path/to/run-request.json --baseline-set /path/to/baselines +# 可选 --output <新目录>;默认输出到 .vchart-visual/runs/ +``` + +三个输入必须配套,不能混用 `--case`、`--dir`、`--baseline`、`--self-compare` 或导出选项。输出须是新目录,位于工作区外或 Git 忽略目录,不能包含或覆盖输入;已有目录不会被覆盖。`suite-manifest.json` 需要 `adapterVersion=1.1.0`,增加实际提交的 `sourceParents` 与 `sourceDirty`;旧导出须重新生成。 + +`run-request.json` 使用公共接口契约 v1 的 `run_request`:固定仓库、产品、被测提交与源摘要、suite/bundle/policy 摘要、环境 ID、assetRevision、selection 和 executionKey。仅支持 `engines=["playwright"]`,Photo 的 suite/environment 为 `null`,截图键为 `final`。`full` 必须包含冻结清单全部用例;`targeted` 必须逐项列出 ID,不扩展为全量。选择摘要为规范化 items 的 SHA-256,执行键为规范化 `{identity, engines, selectionDigest, rerunSequence}` 的 SHA-256。`pr_head` 必须测试 head;`pr_merge` 必须绑定实际父提交 `[baseSha, headSha]`。本机 `local_workspace` 可包含 dirty 修改,但属于辅助证据,不能替代可信 CI 结果。 + +基准目录提供 `baseline-manifest.json`: + +```json +{ + "schemaVersion": 1, + "contractVersion": "1.0", + "kind": "baseline_manifest", + "artifactId": "selected-baselines", + "repository": "VisActor/VChart", + "productKey": "chartspace4", + "environmentId": "trusted-worker-environment-id", + "assetRevision": null, + "renderer": null, + "entries": [ + {"caseId": "bar-stack", "screenshotId": "final", "baselineExpected": false, "path": null, "digest": null} + ] +} +``` + +此示例只声明一个首次基准项。请求目标 `identity.baseRef` 必须为 `develop`;`inputArtifacts` 必须包含该 artifactId、清单**原始文件字节**的 SHA-256、`mediaType=application/json` 和 `visibility=public|restricted`。可见性元信息不赋予执行器服务权限。环境 ID 和 assetRevision 必须与请求一致,所有选中截图均须有唯一条目;路径使用普通相对 POSIX 文件路径,禁止符号链接、越界和内容摘要不符。既有 PNG 填写 `baselineExpected=true`、相对 `path` 与 PNG 的 SHA-256;既有基准意外丢失仍声明 `true`,不得改成首次基准。只要使用既有 PNG,`renderer` 必须精确记录其实际 `{node, platform, arch, playwright, browser, settings}`,并与运行时实测一致。平台环境 ID 的完整镜像、字体、权限、内核和网络约束仍由可信控制器验收,本入口不建立操作系统隔离。 + +基准只复制到结果目录后用于原生 `toMatchSnapshot`,不开启基准更新;运行前后校验输入和副本不变。用例继续执行原有 createSpec、exercise/verify、有效绘制、连续稳定截图和 release。交互后再次检查绘制,空白、资源错误或语义断言失败不能发布候选图。 + +机器结果读取 `run-result.json`(`kind=run_result`、contractVersion=1.0)。逐截图保留三项检查、七类状态 `matched` / `different` / `no_baseline` / `assertion_failed` / `execution_error` / `skipped` / `cancelled`、图像摘要与证据路径;`complete` 仅表示全部选中截图具有终态报告,不表示比较通过。首次基准项经断言和稳定性验证可输出 `role=candidate` 的 PNG,但状态仍为 `no_baseline`、rawOutcome=failed、退出 `2`;意外缺基准另带 `MISSING_BASELINE`。只有完整的原生比较证据可返回 `different` 和退出 `1`;全部匹配返回 `0`;缺图、不完整、执行或清理错误返回 `2`。标准基准的接受、写回与服务接入另由协调流程负责,本入口没有同步或接受命令。 + +`summary.json`、`agent-summary.md` 和三图 HTML 是同次结果的查看入口;其中“完整比较”仍仅表示所选截图均完成有基准的比较。不要用首次候选已验证推断比较通过,也不要把指定基准或自比较称为官方 develop 通过。 + +```sh +# 先完成构建或套件导出,随后运行真实浏览器及失败路径专项 +node --test packages/vchart/scripts/visual-candidate.test.mjs +``` + ## 报告与清理 命令输出 `.vchart-visual/runs//index.html`,浏览器直接打开即可离线查看,无需启动报告服务: @@ -149,3 +215,12 @@ node --test packages/vchart/scripts/visual-test.test.mjs 自检在独立临时目录中验证:相同产物通过、仅候选构建改变颜色产生差异、缺图不自动生成基线、脚本缺失/异常/外部请求/超时及构建命令失败被归类为执行错误。自检结束自动清理目录和服务。 平台验收及实测耗时见 [验收摘要](./cases/ACCEPTANCE.md)。视觉测试不替代单元测试、性能测试及内网 BugServer 的发版前全量测试。 + + +## 本地比较的机器输出 + +`node packages/vchart/scripts/visual-test.mjs --json` 保留默认全量和官方 develop 比较;也可附带现有 `--case`、`--dir`、`--baseline` 或 `--self-compare`。此选项仅用于本地比较,不与列举、单独预检、套件导出或冻结候选模式混用。 + +stdout 只有一个 `schemaVersion=1`、`kind=local_visual_result` 对象,进度写入 stderr。对象保留真实 `exitCode`、`status`、`complete`、精确 `selection`、结果 `counts`、源码 `source.before/after`、实际 `baseline`、环境和三份报告绝对路径。退出码仍为通过 `0`、差异 `1`、错误 `2`;准备失败时未取得的范围、源码或基线为 null,不推断成功。源码摘要覆盖 HEAD、未提交和未跟踪源码,检查完成并清理后再次核对;发生变化记为 `WORKSPACE_CHANGED`、退出 `2`。目录或单例完成仅表示所选用例完成,`--self-compare` 只表示本地稳定性,均不代表官方全量、CI 或合入资格。 + +依赖及浏览器继续由原生预检负责,不自动安装或修复。报告只位于本次返回的运行目录;取消或清理失败保留错误证据。机器契约专项:`node --test packages/vchart/scripts/visual-json.test.mjs`。可安装的公共 `visactor-flow check` 复用此入口,不另行实现截图比较。 diff --git a/packages/vchart/__tests__/visual/cases/README.md b/packages/vchart/__tests__/visual/cases/README.md index ee39f99bbb..653326d228 100644 --- a/packages/vchart/__tests__/visual/cases/README.md +++ b/packages/vchart/__tests__/visual/cases/README.md @@ -59,3 +59,9 @@ node packages/vchart/scripts/visual-test.mjs - 后续开发者或 Agent 独立新增的用例不填写此行;不写空值、TODO、占位或虚构 ID,也不为通过检查伪造 `sourceExample`。来源注释不是执行前提。 - 后续计划将独立新增用例同步到 BugServer,确认同步成功后才补齐服务返回的真实 ID。本期不实现同步命令、任务或状态字段;缺少 ID 不影响本地测试。 - 已有 ID 不应因重命名、移动或改写用例丢失。ID 只用于追溯,不代表已与线上内容实时同步;测试无需访问 BugServer。 + +## 指定基准与首次候选 + +冻结套件执行仍使用同一份显式清单和原用例格式,步骤见 [工具说明](../README.md#冻结候选与指定只读基准)。新增用例保留稳定 ID、真实语义断言和交互状态验证;首次基准由输入清单明确声明 `baselineExpected=false`,不跳过绘制或稳定性检查。通过这些检查的 PNG 只进入运行结果暂存,`no_baseline` 不表示比较通过,也不自动变为正式基准。缺少既有基准、空白、缺图或断言失败必须保留错误。 + +运行器的故障注入和协议反例放在 `scripts/visual-candidate.test.mjs` 的独立副本中,不登记到产品截图清单、不提交破坏性 case。修改公共运行器后仍需执行相关测试和 PR 前官方 develop 全量比较。 diff --git a/packages/vchart/__tests__/visual/cases/interaction/brush-link-data-key.mjs b/packages/vchart/__tests__/visual/cases/interaction/brush-link-data-key.mjs index c72ec3eb4e..7c0a894d6e 100644 --- a/packages/vchart/__tests__/visual/cases/interaction/brush-link-data-key.mjs +++ b/packages/vchart/__tests__/visual/cases/interaction/brush-link-data-key.mjs @@ -5,6 +5,7 @@ import { interactionTarget, interactionFrame } from '../../interaction-helpers.m * 图表类型:common/scatter。关键配置:brushLinkDataKey='id'。 * 场景条件:两个面板中 A/B 的位置互换,框选 A 后目标面板须仍选中 A。 * 最终检查:真实 inBrush/outOfBrush 状态;未执行框选时不能通过。 + * 生命周期:缩放清空原刷选状态后,仍可再次按 id 联动。 */ export default { createSpec() { @@ -79,13 +80,33 @@ export default { }; }, async exercise(page) { - const point = await interactionTarget(page, 'point', 0, 0); - await page.mouse.move(point.x - 18, point.y - 18); - await page.mouse.down(); - await page.mouse.move(point.x + 18, point.y + 18, { steps: 8 }); - await page.mouse.up(); - await page.mouse.move(950, 750); - await interactionFrame(page); + // 先建立真实选择,再验证布局重置及重置后的第二次交互。 + for (const resized of [false, true]) { + if (resized) { + await page.evaluate(async () => { + const chart = window.__visualChart; + const selected = chart.getChart().getAllSeries().flatMap(series => series.getSeriesMark().getGraphics()); + if (!selected.some(graphic => graphic.hasState('inBrush'))) { + throw new Error('Resize regression requires an actual preceding brush selection'); + } + await chart.resize(900, 700); + window.__brushResizeCleared = chart + .getChart() + .getAllSeries() + .flatMap(series => series.getSeriesMark().getGraphics()) + .every(graphic => !graphic.hasState('inBrush') && !graphic.hasState('outOfBrush')); + await chart.resize(1000, 800); + }); + await interactionFrame(page); + } + const point = await interactionTarget(page, 'point', 0, 0); + await page.mouse.move(point.x - 18, point.y - 18); + await page.mouse.down(); + await page.mouse.move(point.x + 18, point.y + 18, { steps: 8 }); + await page.mouse.up(); + await page.mouse.move(950, 750); + await interactionFrame(page); + } }, async verify(page) { await verifyRendered(page); @@ -94,7 +115,7 @@ export default { const points = target.getSeriesMark().getGraphics(); const a = points.find(p => p.context.data[0].id === 'A'), b = points.find(p => p.context.data[0].id === 'B'); - if (!a.hasState('inBrush') || !b.hasState('outOfBrush')) { + if (!window.__brushResizeCleared || !a.hasState('inBrush') || !b.hasState('outOfBrush')) { throw new Error('Brush must link the same observation id across differently positioned panels'); } }); diff --git a/packages/vchart/__tests__/visual/playwright.config.mjs b/packages/vchart/__tests__/visual/playwright.config.mjs index 17caa4b118..ab90ca2a5d 100644 --- a/packages/vchart/__tests__/visual/playwright.config.mjs +++ b/packages/vchart/__tests__/visual/playwright.config.mjs @@ -5,6 +5,8 @@ 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 运行'); +if (process.env.VISUAL_EXTERNAL_BASELINE === '1' && phase !== 'current') + throw new Error('指定基准模式只运行候选侧,不允许更新基准'); export default defineConfig({ testDir: '.', diff --git a/packages/vchart/__tests__/visual/report.mjs b/packages/vchart/__tests__/visual/report.mjs index 865e9ab63e..2bbb51a09d 100644 --- a/packages/vchart/__tests__/visual/report.mjs +++ b/packages/vchart/__tests__/visual/report.mjs @@ -23,6 +23,8 @@ function selectionDescription(report) { const label = selection.mode === 'all' ? '全部本地用例' + : selection.mode === 'selection' + ? '指定集合 ' + selection.value : `${selection.mode === 'directory' ? '目录' : '单用例'} ${selection.value}`; return `${label};选中 ${selection.selectedCount} / ${selection.totalCount};其余 ${ selection.totalCount - selection.selectedCount @@ -298,9 +300,14 @@ function agentSummary(report) { /** 生成无需服务和网络的三图报告;原图链接保留完整分辨率。 */ function renderHtml(report) { - const labels = { passed: '通过', diff: '视觉差异', error: '执行错误', not_run: '未完成' }; + const labels = { passed: '通过', diff: '视觉差异', error: '执行错误', not_run: '未完成', no_baseline: '无指定基准' }; const link = (file, label) => `${escape(label)}`; - const baseline = report.baseline?.repository === 'working-tree' ? '工作区自比较' : '官方基线'; + const baseline = + report.baseline?.repository === 'frozen-images' + ? '指定冻结基准' + : report.baseline?.repository === 'working-tree' + ? '工作区自比较' + : '官方基线'; const cards = report.cases .map( item => `
@@ -391,9 +398,9 @@ function renderHtml(report) { ) .join('')}