Pts 消费者集成测试实战:从 npm 包到 React、Vue、Node 与原生浏览器的全矩阵验证
前端【免费下载链接】ptsA library for visualization and creative-coding项目地址https://gitcode.com/gh_mirrors/pt/pts点击查看免费下载导读本文围绕 Pts 仓库的 plans/INTEGRATION-TEST-PLAN.md 集成测试方案系统讲解如何为发布产物设计消费者视角的兼容性测试以打包后的 npm tarball 为唯一被测对象在 React Vite、Vue Vite、Node skia-canvas与单文件原生 HTML 四种环境中验证 Pts 的真实行为而非仅仅证明框架能渲染一个标题。读完本文你将掌握集成测试的目录结构设计、打包隔离模型、环境矩阵与断言策略、Runner 编排细节以及从这套测试中实际发现并修复的CanvasSpace.dispose()缺陷。一、为什么要做消费者集成测试1.1 目标定位兼容性夹具而非产品示例集成测试计划的 Objective 写得非常明确These are compatibility fixtures, not product examples. Each must verify real Pts behavior after installation from an npm tarball, not merely prove that its framework can render a heading.也就是说每个夹具都必须满足两条硬性约束安装来源是 npm tarball测试的是发布出去的包而不是源码检出目录验证真实 Pts 行为框架页面里必须真正构造CanvasSpace、真正用 Pts 的表单Form绘制内容而不只是import一下再渲染一个标题。1.2 与既有质量门禁的分工计划的 Package isolation model 一节明确指出集成测试不与现有包级检查重复The existing package audit remains responsible for the exact published-file allowlist, CJS, NodeNext, and ATTW/publint. These integration tests add consumer breadth rather than duplicating every package test.即已发布的文件白名单、CJS 兼容、NodeNext 解析、类型声明一致性ATTW/publint由包审计负责对应 scripts/check-artifacts.mjs 中expectedFiles与expectedExports的硬编码清单集成测试专司消费者广度不同框架、打包器、模块系统、运行时环境能否正确消费这个包。从根 package.json 的exports字段可以看到 Pts 发布的包契约import条件指向./dist/index.mjs类型./dist/index.d.mtsrequire条件指向./dist/index.js类型./dist/index.d.tsunpkg与jsdelivr指向 IIFE 产物./dist/pts.min.js。集成测试矩阵中的四个夹具恰好分别覆盖这张契约的四条路径。二、包管理器决策根仓库 pnpm 嵌套私有 npm workspace2.1 决策内容计划规定根仓库的 Pts 开发流程使用 pnpm根 package.json 中packageManager: pnpm11.21.0在test/integrations下维护一个嵌套的、私有的 npm workspace自带package.json与package-lock.json根 Pts 包不转成 workspace 或 monorepo夹具 workspace 仅作为测试基础设施存在。2.2 这样设计的原因从计划原文可以提炼出三点动机依赖隔离框架专用依赖React、Vue、Vite、skia-canvas等不会进入根 pnpm 依赖图也就不会污染发布包路径演练npm workspace 本身就是 npm 消费者的安装路径用它来演练真实安装过程可复制性package-lock.json被提交进仓库保证npm ci在 CI 与本地得到一致结果。看 test/integrations/package.json 的实际内容可以印证workspace 名为pts-integrations/workspace、private: true声明了react-vite、vue-vite、skia-canvas三个子 workspacevanilla 是纯静态 HTML不参与 npm workspace并固定packageManager: npm11.16.0。2.3 allowScripts原生依赖的显式授权test/integrations/package.json 中有一个非常值得注意的字段allowScripts: { fsevents2.3.3: true, skia-canvas3.0.8: true }这是实现结果中提到的explicitly reviewed and version-pinned in npmsallowScriptspolicyskia-canvas需要安装脚本编译原生绑定Vite 的fsevents是 macOS 可选依赖两者的 install script 都被精确钉版本后显式放行。这种做法避免安装脚本静默失败或依赖悄悄更换版本引入新脚本属于对原生依赖的供应链管控。三、目录结构规划计划给出的目标结构见 test/integrations 目录为test/integrations/ ├── README.md ├── package.json ├── package-lock.json ├── react-vite/ │ ├── index.html │ ├── package.json │ ├── tsconfig files │ ├── vite.config.ts │ └── src/ ├── vue-vite/ │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── vite.config.ts │ └── src/ ├── skia-canvas/ │ ├── package.json │ └── render.mjs └── vanilla/ └── index.html关键约束vanilla 必须保持单一 HTML 文件内联 CSS、内联 JavaScript用经典script引用已安装的pts.min.js全程不被 Vite 转换应用逻辑留在各自夹具内Runner 只负责编排新增环境通常意味着新增一个夹具目录 Runner 里一行矩阵条目而不是把源代码以字符串形式嵌入 Runner。test/integrations/README.md 把如何新增环境固化为六条准则精确钉依赖版本、通过file:../../..依赖 Pts、覆盖独特的包/运行时表面、提供确定性的构建或 Node 测试命令、浏览器夹具暴露可断言的 ready 标记/帧数/像素/销毁状态、输出只落在dist或临时目录。四、打包隔离模型只测发布出去的东西4.1 开发态与测试态的两条依赖路径开发态提交的 workspace manifest 用file:../../..指向 Pts便于维护者在本地直接针对当前仓库跑夹具test/integrations/README.md 中的npm ci npm run build npm run test:node测试态自动化 Runner 采用严格路径完整流程见 scripts/check-integrations.mjsnpm pack --silent --pack-destination tmp生成全新 tarballtar --extract --gzip --strip-components1解压到临时 Pts 包根目录——该目录只包含 npm 实际发布的内容把test/integrations复制到解压根下的test/integrations保留提交的file:../../..相对关系对嵌套 lockfile 执行npm ci此时本地 Pts 链接指向解压出来的包而非源码检出断言安装的 Pts 真实路径落在临时包根内且 Node ESM 解析到dist/index.mjs从该临时、锁定的安装环境构建并运行全部夹具即使测试失败也通过try/finally清理临时 workspace、HTTP 服务器与浏览器进程。4.2 防假测试的三重断言Runner 用三个断言堵死测试意外用到 registry 版本或源码检出版本的漏洞对应计划 gap 第 13 条assert.equal(await realpath(installedPts), await realpath(extractedRoot)); const installedManifest JSON.parse( await readFile(path.join(installedPts, package.json), utf8), ); assert.equal(installedManifest.name, pts); // Node ESM 解析结果必须等于 临时包根的 dist/index.mjs assert.equal( resolvedPts, pathToFileURL(await realpath(path.join(extractedRoot, dist, index.mjs))).href, ); assert.ok(!resolvedPts.includes(pathToFileURL(projectRoot).href));realpath比较workspace 链接的 pts 目录真实路径 解压出来的包根manifest 校验确认安装的是名为pts的包ESM 解析校验import.meta.resolve(pts)的结果必须指向临时包根的dist/index.mjs且不包含源码检出的路径。完成这一组断言后Runner 依次执行react-vite与vue-vite的npm run build --workspace、skia-canvas的npm test --workspace再启动 Playwright Chromium 做浏览器断言。五、环境矩阵与断言设计5.1 矩阵总览计划中的矩阵表定义了四个夹具及其消费者表面与必需断言夹具消费者表面必需断言React Vite TSESM 条件导出与类型声明生产构建成功真实 Chromium 挂载 ReactCanvasSpace进入 ready、绘制像素、运行动画并在组件卸载时销毁Pt行为可见于页面状态无浏览器/页面错误Vue Vite TSESM 条件导出与类型声明生产构建成功真实 Chromium 挂载 Vue Composition API 组件同样的像素与显式卸载/销毁生命周期通过无浏览器/页面错误skia-canvas Node ESM非 DOM Node canvas 实现中的 ESM 导入CanvasForm接受 Skia 2D context几何与绘制调用完成PNG 字节有合法签名且非平凡大小不提交输出图片Vanilla 单 HTML发布的压缩 IIFE 与globalThis.Pts静态服务器原样服务 HTML 与已安装的pts.min.jsChromium 观察到全局对象创建/绘制/动画/销毁CanvasSpace暴露确定性的通过状态React 与 Vue 使用 create-vite 风格的 TypeScript 脚手架和官方 Vite 插件并且刻意使用生产构建——测试的是优化后的 ESM 消费路径而不是开发服务器的转换路径。5.2 版本钉定Implementation outcome实现于 2026-08-10消费者版本全部精确固定React 19.2.8、React DOM 19.2.8、vitejs/plugin-react6.0.5Vue 3.5.41、vitejs/plugin-vue6.0.8、vue-tsc3.3.9两个框架夹具统一 Vite 8.2.1、TypeScript 5.9.3。TypeScript 6/7没有强行推入 Vue 夹具因为当前vue-tsc版本仍解析 legacy compiler 子路径而新 TS 版本会封死该子路径skia-canvas3.0.8其 install script 与 Vite 可选的fseventsscript 在 npmallowScripts策略中显式审查并钉版本。各 manifest 印证了钉定细节react-vite/package.json、vue-vite/package.json、skia-canvas/package.json。六、夹具实战解析6.1 React Vite TypeScriptuseEffect 生命周期内建/拆 Ptsreact-vite/src/App.tsx 展示了标准用法useEffect(() { const canvas canvasRef.current; if (!canvas) return; let active true; let frames 0; const space new CanvasSpace(canvas, () { if (!active) return; const form space.getForm(); space.add({ animate: () { frames 1; canvas.dataset.frames String(frames); form.fillOnly(#f97316).point([160, 90], 14, circle); }, }); space.play(); }).setup({ bgcolor: #0f172a, resize: false, retina: false }); return () { active false; space.stop(); space.dispose(); setTimeout(() onDisposed(!space.isPlaying), 50); }; }, [onDisposed]);要点回调式初始化CanvasSpace的第二个参数是 canvas 挂载就绪后的回调内部再space.add(...)与space.play()显式清理effect cleanup 中先stop()再dispose()并通过setTimeout回报!space.isPlaying让销毁是否生效成为可断言的状态应用状态可见性App用new Pt(1, 2).add(3).toString()得到Pt(4, 5)写入#status的data-point从而在页面 DOM 中验证 Pt 运算行为二次挂载toggleCanvas驱动mounted状态实现卸载→重挂载→再卸载的完整生命周期验证入口 react-vite/src/main.tsx 还启用了 ReactStrictMode让双挂载效应天然暴露泄漏。构建命令为tsc --noEmit vite buildvite 配置仅注册官方 react 插件react-vite/vite.config.ts。6.2 Vue Vite TypeScriptonMounted / onBeforeUnmount 生命周期vue-vite/src/PtsCanvas.vue 采用 Composition APIonMounted(() { if (!canvas.value) return; const element canvas.value; let frames 0; space new CanvasSpace(element, () { if (!active || !space) return; const form space.getForm(); space.add({ animate: () { frames 1; element.dataset.frames String(frames); form.fillOnly(#f97316).point([160, 90], 14, circle); }, }); space.play(); }).setup({ bgcolor: #0f172a, resize: false, retina: false }); }); onBeforeUnmount(() { active false; if (!space) return; const disposedSpace space; disposedSpace.stop(); disposedSpace.dispose(); setTimeout(() reportDisposed(!disposedSpace.isPlaying), 50); });App.vue通过v-ifmounted控制组件挂载data-disposed记录销毁状态。构建命令vue-tsc --noEmit vite build保证类型检查与生产构建双通过vue-vite/package.json、vue-vite/vite.config.ts。6.3 skia-canvas Node ESM无 DOM 的服务端绘制skia-canvas/render.mjs 是纯 Node ESM 测试脚本核心在于Skia 不是 DOM 实现不能构造CanvasSpace而是直接测试CanvasForm与标准兼容的 2D contextconst canvas new Canvas(320, 180); const context canvas.getContext(2d); const form new CanvasForm(context); assert.equal(form.ready, true); form.fillOnly(#f97316).point(new Pt(160, 90), 14, circle); form.strokeOnly(#38bdf8, 4).line([[20, 20], [300, 160]]); form.fillOnly(#a3e635).circle(Circle.fromCenter([60, 120], 18)); assert.equal(new Pt(1, 2).add(3).toString(), Pt(4, 5)); const png await canvas.png; assert.ok(png.length 1_000, PNG output is too small: ${png.length} bytes); assert.deepEqual([...png.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]);断言层面验证了CanvasForm接受 Skia contextform.ready true、几何运算正确Pt(4, 5)、PNG 输出非平凡大小且文件头为合法 PNG 签名89 50 4E 47 0D 0A 1A 0A。同时计划明确no output image is committed——渲染产物只存在于内存不污染仓库。6.4 Vanilla 单文件验证 IIFE 产物与浏览器全局vanilla/index.html 是唯一不经过任何打包器转换的夹具结构为单 HTML内联 CSS、script src/pts/pts.min.js经典脚本引用、内联业务脚本。业务脚本通过globalThis.Pts命名空间使用库status.dataset.point new Pts.Pt(1, 2).add(3).toString(); const space new Pts.CanvasSpace(canvas, () { const form space.getForm(); space.add({ animate: () { frames 1; canvas.dataset.frames String(frames); form.fillOnly(#f97316).point([160, 90], 14, circle); }, }); space.play(); }).setup({ bgcolor: #0f172a, resize: false, retina: false }); document.querySelector(#toggle).addEventListener(click, () { space.stop(); space.dispose(); setTimeout(() { status.dataset.disposed String( !space.isPlaying Object.keys(space.players).length 0, ); }, 50); });销毁状态在此处是!space.isPlaying Object.keys(space.players).length 0比框架夹具多验证了players 列表已清空这一层。七、Runner 编排设计7.1 职责边界只做编排不做应用逻辑计划指定一个根脚本 scripts/check-integrations.mjs 专司编排职责清单pack、解压并准备隔离 workspace用提交的 lockfile 安装精确的夹具依赖断言 tarball 模块解析调用各夹具自己的 build/test 脚本用进程内 HTTP 服务器在临时端口服务构建产物/静态文件复用根仓库已有的 Playwright 安装做浏览器断言收集页面错误与失败的网络响应用try/finally保证清理。7.2 内置静态服务器startServer基于node:http实现以127.0.0.1监听0端口操作系统分配临时端口天然避免端口冲突并做两件关键事路径逃逸防护解析请求路径后断言其落在 fixture 根目录内否则拒绝request escaped the fixture root路由覆盖vanilla 夹具通过routesMap 把/pts/pts.min.js直接映射到临时包根的dist/pts.min.js保证服务的是已发布包里的 IIFE 产物而不是源码编译产物。7.3 浏览器断言流程PlaywrighttestBrowserFixture是浏览器端通用断言函数流程如下错误监听收集console.error、pageerror、失败的本地请求requestfailed与非 2xx 响应response最后断言错误列表为空ready 轮询waitForFunction在 10 秒超时内等待三个条件同时成立——#status的data-environment匹配、data-point Pt(4, 5)、canvas.dataset.frames 0。这里采用轮询应用自有 ready 标记 有界超时而非精确帧数计数就是为了规避动画时序抖动gap 第 7 条像素级验证getImageData(160, 90, 1, 1)读取画布中心像素断言 RGBA 接近 Pts 绘制色#f97316R150、G50、B100、A255——画了动画帧但实际没画出来的假阳性由此堵死gap 第 17 条vanilla 专属断言typeof globalThis.Pts object验证 IIFE 暴露了浏览器全局生命周期测试点击#toggle触发条件卸载等待data-disposed true对 React/VueremovesCanvas: true额外断言[data-pts-canvas]数量归零再点击#toggle重挂载验证重新绘制像素然后再次卸载最后assert.deepEqual(errors, [])。这套流程对应了计划中React 和 Vue 额外完成第二次 mount/draw/unmount 循环捕获泄漏的动画或 DOM 状态的实现结果。7.4 根命令暴露根 package.json 提供两条命令test:integrationsnode scripts/check-integrations.mjs跑完整的打包矩阵check在常规 build/package 门禁之后串联pnpm test:integrations。Runner 从 lockfile 安装临时嵌套 workspace因此在根仓库安装完成后pnpm test:integrations是自包含的。CI 在固定的主 Node 版本上运行它既有的 Node 20/22/24 运行时矩阵继续专注于轻量的发布 ESM/CJS 运行时检查框架工具链不在三个 job 上重复铺开。八、Plan Gap Review18 个已知风险与对策计划在实现前系统性地排查了 18 个潜在漏洞这里按主题归并打包与解析真实性workspace 符号链接可能掩盖发布失败 → 用临时 workspace 中的打包安装替换测试可能意外用到 registry 或源码检出版本 → 在本地依赖目标处解压 tarball、比较真实路径、检查pts/package.json、断言解析出的模块路径临时安装可能绕过 lockfile 却改变 Pts 路径 → 保留本地依赖 spec 与目录关系在目标处解压并用npm ci原样安装Skia 不是 DOM 实现 → 用标准兼容 2D context 测CanvasForm不构造需要 DOM 的CanvasSpace原生依赖可能与 JS 依赖以不同方式失败 → 钉住skia-canvas、放行其受支持的安装机制、失败显式化CI 不静默跳过。构建 ≠ 运行时2. 成功的 Vite 构建不能证明运行时行为 → React/Vue 构建必须在 Chromium 中跑CanvasSpace生命周期与绘制 17. canvas API 可能运行了却没画出东西 → 动画后读取已知像素而非只看帧数 3. 框架代码可能只测了 Pts 几何 → 两个组件必须构造真实 Pts canvas 并使用 Pts form。vanilla 与打包器边界4. Vite 服务的 vanilla 文件不是真 vanilla → 无转换地服务单 HTML经典 script URL 直接映射到已安装包。测试稳定性与泄漏7. 动画时序导致 flaky → 有界超时轮询应用自有 ready 标记断言至少一帧而非精确帧数 11. 夹具输出污染仓库 → 只在临时副本中构建并防御性扩展 ignores 12. 端口与浏览器进程在失败后泄漏 → 临时端口finally中关闭每个 server/page/browser。生命周期与错误可见性8. React cleanup 被生产环境专属行为隐藏 → 从应用暴露 cleanup/销毁状态测试显式 unmount/remount 生命周期 9. 浏览器控制台错误易漏 →pageerror、意外 console 错误、失败本地资源请求全部 fail 18. cleanup 断言可能在无框架生命周期参与下通过 → 从浏览器测试触发条件组件移除并从 React/Vue 的 cleanup hook 暴露销毁状态 10. 嵌套夹具依赖泄漏进 npm 发布 → 置于根依赖图之外保留精确的打包文件白名单检查。演进与依赖管理14. 未来夹具沦为复制粘贴示例 → 文档化新增夹具的最低 package/build/runtime/cleanup 断言要求 15. 依赖更新悄悄改变矩阵 → 精确钉定工具/框架版本并提交嵌套 lockfile。九、真实产出发现的 Pts 缺陷与修复Implementation outcome 记录了这套测试最重要的价值——抓到一个真实的 Pts bugThe suite found one real Pts defect:CanvasSpace.dispose()assumed that aResizeObserverexisted even whenresize: false. The implementation now guards that optional resource, and all three browser fixtures exercise the fixed path.即CanvasSpace.dispose()在resize: false时仍假定ResizeObserver存在并尝试断开它。查看 src/Canvas.ts 的修复后实现可以确认dispose(): this { if (this._disposed) return this; this._disposed true; if (this._readyTimer ! undefined) { window.clearTimeout(this._readyTimer); this._readyTimer undefined; } if (this._readyObserver) { this._readyObserver.disconnect(); this._readyObserver undefined; } if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver undefined; } this._unbindAll(); this._cancelAnimation(); this.removeAll(); this._isReady false; if (this._ownsCanvas) this._canvas.remove(); if (this._ownsContainer this._container.childNodes.length 0) this._container.remove(); return this; }修复要点_disposed标志保证 dispose 幂等重复调用直接返回_resizeObserver现在被可选守卫if (this._resizeObserver)包裹与_readyObserver同样处理这与 src/Canvas.ts 中autoResizesetter 的逻辑一致只有auto为真时才创建ResizeObserverthis._resizeObserver new ResizeObserver(...)因此 dispose 时必须容忍它不存在。三个浏览器夹具React、Vue、vanilla的setup({ resize: false })全部覆盖了这条修复路径这也是矩阵设计每个夹具都要真实走 Pts 生命周期的回报。十、运行方式与退出标准10.1 如何运行从仓库根目录运行完整打包矩阵测试pnpm test:integrations或作为完整质量门禁的一部分构建、包检查、发布检查之后执行pnpm check本地针对当前源码检出做夹具开发调试进入 test/integrations 目录后npm ci npm run build npm run test:node10.2 退出标准全部满足四个夹具已提交、可读且能通过一条根命令运行React 与 Vue 通过 typecheck、build并从 tarball 通过真实浏览器 Pts 生命周期测试skia-canvas在 Node 中渲染 Pts 输出并校验生成 PNGVanilla 保持单一未转换 HTML 文件使用 IIFE 产物在 Chromium 中通过临时安装解析到精确的打包 Pts 包及其 ESM 入口根check与 CI 包含新套件重复运行后除预期的 Pts 现代化改动外三个仓库保持干净。结语Pts 的集成测试计划提供了一个可复用的消费者验证范式用打包产物而非源码做被测对象、用隔离临时 workspace 防假阳性、用像素与生命周期断言防假阴性、用错误收集与 finally 清理保稳定。它的工程价值不仅在于证明Pts 能在 React/Vue/Node/vanilla 中跑起来更在于——通过resize: false时CanvasSpace.dispose()的崩溃证明了只有真正以消费者身份、在真实浏览器里跑真实绘制与销毁才能发现单元测试难以触及的集成缺陷。对于任何发布 npm 库的团队这份计划的矩阵划分、gap 审查清单与 Runner 编排思路都值得直接借鉴。赞分享前端【免费下载链接】ptsA library for visualization and creative-coding项目地址https://gitcode.com/gh_mirrors/pt/pts点击查看免费下载相关推荐AssetRipper 免费 Unity 资产提取工具十分钟把游戏文件还原成能打开的工程AssetRipper 免费 Unity 资产提取工具十分钟把游戏文件还原成能打开的工程 AssetRipper 是一款免费开源的 Unity 资产提取工具开发工具逆向工程游戏开发WebdriverIO跨浏览器测试矩阵自动化测试覆盖与兼容性报告生成WebdriverIO跨浏览器测试矩阵自动化测试覆盖与兼容性报告生成 一、测试矩阵构建痛点与解决方案 1.1 企业级测试的三大核心挑战 碎片化环境 全球浏览测试质量保障NProgress 测试与发布全流程从 npm 测试、浏览器手动验证到 Component 构建与版本发布NProgress 测试与发布全流程从 npm 测试、浏览器手动验证到 Component 构建与版本发布 本篇指南以 NProgress 仓库的 Notes前端UI组件上一篇tick-stock-panel价格提醒教程3步设置高低价突破监控与盘中弹窗下一篇treg run --server 远程执行原理密钥永不到达你机器的完整机制6层防护拆解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考