diff --git a/docs/assets/ts/artplayer-plugin-chapter.d.ts b/docs/assets/ts/artplayer-plugin-chapter.d.ts index 5d6f7b378..950d9cbc3 100644 --- a/docs/assets/ts/artplayer-plugin-chapter.d.ts +++ b/docs/assets/ts/artplayer-plugin-chapter.d.ts @@ -13,7 +13,7 @@ interface Result { update: (option: Option) => void } -declare const artplayerPluginChapter: (option: Option) => (art: Artplayer) => Result +declare const artplayerPluginChapter: (option?: Option) => (art: Artplayer) => Result export default artplayerPluginChapter diff --git a/packages/artplayer-plugin-chapter/README.md b/packages/artplayer-plugin-chapter/README.md index 65ffbbbb8..4a5e3a0c8 100644 --- a/packages/artplayer-plugin-chapter/README.md +++ b/packages/artplayer-plugin-chapter/README.md @@ -6,6 +6,36 @@ chapter plugin for ArtPlayer [https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-chapter/index.js&example=chapter) +## API + +```js +import artplayerPluginChapter from 'artplayer-plugin-chapter' + +const plugin = artplayerPluginChapter({ + chapters: [{ start: 0, end: Infinity, title: 'Introduction' }], +}) +// Pass plugin to Artplayer's plugins array. Options may also be omitted. +``` + +The registered name is `artplayerPluginChapter`. After metadata loads, use +`art.plugins.artplayerPluginChapter.update({ chapters })` to replace the chapters, or +`update({})` to clear them. Update is synchronous and requires an options object. +Chapter times are seconds; ranges must not overlap. `Infinity` as an end value uses the current duration. + +The existing implementation sorts and fills the supplied array in place and replaces its Infinity end +values. Pass a fresh array when you need to retain the original configuration. After switching media, +call update with chapters appropriate to the new duration; initialization listens to metadata once. + +## Maintenance + +The implementation is currently in `src/index.js`, styles in `src/style.less`, and public declarations +in `types/artplayer-plugin-chapter.d.ts`. Its migration contracts and known defects are recorded in +[the chapter contract](../../refactor/baselines/chapter-contract.md). + +Run `yarn test:browser chapter.spec.js` for published/current plugin behavior on the published core. +Run `yarn typecheck` and `yarn test:baseline` for strict consumer checks, including optional factory +arguments and invalid data. `yarn build:ts` regenerates the online editor declarations. + ## License MIT © Harvey Zhao diff --git a/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts b/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts index ea1d72f2f..73f8a2611 100644 --- a/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts +++ b/packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts @@ -15,6 +15,6 @@ interface Result { update: (option: Option) => void } -declare const artplayerPluginChapter: (option: Option) => (art: Artplayer) => Result +declare const artplayerPluginChapter: (option?: Option) => (art: Artplayer) => Result export default artplayerPluginChapter diff --git a/refactor/baselines/chapter-validation.json b/refactor/baselines/chapter-validation.json new file mode 100644 index 000000000..b46f1cdc6 --- /dev/null +++ b/refactor/baselines/chapter-validation.json @@ -0,0 +1,329 @@ +{ + "task": "PKG-CHAPTER-02", + "capturedAt": "2026-09-10T14:39:42.386Z", + "node": "24.21.0", + "platform": "win32", + "sourceHashAlgorithm": "sha256-lf", + "hashes": { + "packages/artplayer-plugin-chapter/src/index.js": "fcd732e4257597a8cbb192fa4d715488b344df361a3a5b7d0e7c70f7554a73e6", + "packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts": "15856abc93ef140cf0c589c30f80335e5395b30b4604bc040e7e241281882127", + "docs/assets/ts/artplayer-plugin-chapter.d.ts": "2931630e9fca67ddf3b3b4018b7cce8df880c6528a4d34140bbacc15abab54d8", + "test/browser/chapter.spec.js": "a13aca482cf25eea4c240782262c597192781b763425d9da233275bd1d02c3c2", + "test/browser/player.html": "4037c6eadc5da176f9dd67b2b517acb06c8cb2246c841316df26c660e55584b4", + "test/browser/playback.spec.js": "5cc5df6f3968f3e834a6b2bc2c76c3c1c4ec5e7bda5ee3e99aa7394d24ac7e18", + "test/types/chapter-options.ts": "8ae16ff704befdcf5303012d15d108bad4aa7dab4d62da9c02cff4776680fe60", + "refactor/scripts/types.test.mjs": "17a4c9a2c2ae7cebe9c3007aad090f01371d8c7f65feb98662db08fc7cf619d8" + }, + "resources": { + "/candidate/artplayer.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.js" + }, + "sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290", + "bytes": 220281 + }, + "/compiled/artplayer.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.js" + }, + "sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290", + "bytes": 220281, + "aliasOf": "/candidate/artplayer.js" + }, + "/uncompiled/artplayer/index.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer\\src\\index.js" + }, + "sha256": "c6f564431e03a198c81fc026af75ead9f7c624e5c7f2f23cba5ce984ec045290", + "bytes": 220281, + "aliasOf": "/candidate/artplayer.js" + }, + "/candidate/artplayer-plugin-chapter.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.js" + }, + "sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94", + "bytes": 8178 + }, + "/compiled/artplayer-plugin-chapter.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.js" + }, + "sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94", + "bytes": 8178, + "aliasOf": "/candidate/artplayer-plugin-chapter.js" + }, + "/uncompiled/artplayer-plugin-chapter/index.js": { + "source": { + "kind": "workspace-build", + "entry": "packages\\artplayer-plugin-chapter\\src\\index.js" + }, + "sha256": "833b81bf3334443531ed1822e28a38a241484056c6dbbdd50cd1ec093303ab94", + "bytes": 8178, + "aliasOf": "/candidate/artplayer-plugin-chapter.js" + }, + "/published/artplayer.js": { + "source": { + "kind": "npm-release", + "version": "5.4.0", + "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==", + "member": "package/dist/artplayer.js" + }, + "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f", + "bytes": 134037 + }, + "/published/artplayer-plugin-chapter.js": { + "source": { + "kind": "npm-release", + "version": "1.1.0", + "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==", + "member": "package/dist/artplayer-plugin-chapter.js" + }, + "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0", + "bytes": 4973 + }, + "/test/pattern.mp4": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/pattern.mp4" + }, + "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233", + "bytes": 251538 + }, + "/test/pattern.webm": { + "source": { + "kind": "generated-media", + "file": "test/browser/media/pattern.webm" + }, + "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f", + "bytes": 162073 + }, + "/assets/sample/video.mp4": { + "source": { + "kind": "docs-media", + "file": "docs/assets/sample/video.mp4" + }, + "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980", + "bytes": 5818706 + } + }, + "fullBrowserSuite": { + "startTime": "2026-09-10T14:39:42.386Z", + "duration": 21337.515, + "expected": 39, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "tests": [ + { + "scope": "chapter.spec.js / published-only observations: blank title, NaN and once-only metadata are historical defects", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [], + "observation": { + "staleTitle": "Previous", + "endAfterSourceChange": 8, + "acceptsNaN": true + } + }, + { + "scope": "chapter.spec.js / published-only observations: blank title, NaN and once-only metadata are historical defects", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [], + "observation": { + "staleTitle": "Previous", + "endAfterSourceChange": 8, + "acceptsNaN": true + } + }, + { + "scope": "chapter.spec.js / published-only observations: blank title, NaN and once-only metadata are historical defects", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [], + "observation": { + "staleTitle": "Previous", + "endAfterSourceChange": 8, + "acceptsNaN": true + } + }, + { + "scope": "chapter.spec.js / published chapter on published core / factory, registration, input normalization and safe title text", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / explicit source update and core-owned destruction", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / factory, registration, input normalization and safe title text", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / explicit source update and core-owned destruction", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / factory, registration, input normalization and safe title text", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / published chapter on published core / explicit source update and core-owned destruction", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / factory, registration, input normalization and safe title text", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / explicit source update and core-owned destruction", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / factory, registration, input normalization and safe title text", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / explicit source update and core-owned destruction", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / factory, registration, input normalization and safe title text", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / progress bars, real hover, title edges and boundary seek", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / sync update, Infinity, object identity, empty lists and invalid data", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + }, + { + "scope": "chapter.spec.js / candidate chapter on published core / explicit source update and core-owned destruction", + "project": "webkit", + "status": "passed", + "browser": "26.6", + "errors": [] + } + ] +} diff --git a/refactor/changes/2026-09-10-PKG-CHAPTER-02-tests.md b/refactor/changes/2026-09-10-PKG-CHAPTER-02-tests.md new file mode 100644 index 000000000..213a8e087 --- /dev/null +++ b/refactor/changes/2026-09-10-PKG-CHAPTER-02-tests.md @@ -0,0 +1,42 @@ +# PKG-CHAPTER-02:章节回归与可选参数声明 + +## 改动 + +新增 [chapter.spec.js](../../test/browser/chapter.spec.js),在已发布核心 5.4.0 上分别运行发布与当前 +chapter:每浏览器 8 项正常/边界/失败/生命周期测试,另加 1 项只针对发布包的历史缺陷观察。 +实际鼠标 hover、进度点击 seek、metadata 与切源由真实浏览器执行;没有替换 HTMLMediaElement。 + +覆盖无参工厂完整注册、name/结果 key、同步 update、数组原地排序/补齐/对象引用、Infinity、 +标题 trim 和 HTML 纯文本、gap、三类进度条、标题两侧夹紧、边界 seek、空配置、六类非法数据、 +无参 update 错误、切源后显式 update、destroy 和全局 style 保留。 + +生产声明的工厂参数从必填扩展为可选,运行时已有默认值,不改变旧调用;update 的对象仍必填。 +[消费者夹具](../../test/types/chapter-options.ts) 验证无参/undefined/空对象/正常调用及三个反例。 +运行 build:ts 再生成编辑器声明,仅 chapter 文件有实际差异。README 补调用说明与测试入口。 +没有迁移或修改插件 JS 运行时;生产源码 TS 迁移仍为零。 + +## 验证和发现 + +- Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6:chapter 27 项通过;连同原 smoke 总计 39 项通过。 +- TS 5.9.3 的 Node10 CommonJS / NodeNext CommonJS / Bundler ESM 与 TS 4.3.5 旧消费通过, + 错误 update/标题/null 参数仍被拒绝。关闭 BASE-TYPE-02;BASE-TYPE-01/03 不受本修复影响。 +- `yarn ci:check` 55 项检查通过;build:ts、计划/风险台账、Git 差异检查通过。 +- [运行记录](../baselines/chapter-validation.json) 保存源码/声明指纹、浏览器版本、结果及历史观察。 + +实际复现空白 gap 沿用上一标题、NaN 区间进入 DOM、切源后保留旧区间。历史观察仅对发布版本 +固定这些结果,不要求候选永久保持缺陷。PKG-CHAPTER-03 负责标题/非法时间修正、生命周期与 +模块拆分;切源后的显式更新责任和已有调用方数组变化保持可解释,不能意外改变公开语义。 + +测试使用真实鼠标来提供核心 hover 事件需要的原生 event;整数坐标决定实际百分比。同步 +setBar 断言在一次 page.evaluate 中发出并读取,避免与原生 timeupdate 交错导致测试误报。 +完整套件另复现 Firefox 已缓存媒体 Range 的 NS_ERROR_PARSED_DATA_CACHED;按 +[Mozilla 的说明](https://bugzilla.mozilla.org/show_bug.cgi?id=1347174#c4) 增加精确引擎/媒体类型/同源/ +用例 URL 限定,保留原始记录和全部播放断言,不全局忽略请求失败。 + +## 边界和下一步 + +仍未验收完整编辑器、移动真机、所有核心历史版本或最终 tarball;旧核心/新插件的范围以当前 +基线为准。类型消费者的 NodeNext ESM/legacy 解析归 PKG-CHAPTER-04。先完成 ENG-07 候选消费, +再进入 PKG-CHAPTER-03 源码拆分;可直接用 TS 实现模块,04 继续完成公开类型/分发消费闭环。 + +回退本提交恢复原必填声明及编辑器输出,并移除新增测试/文档;旧合法调用和运行时代码不变。 diff --git a/refactor/plan.md b/refactor/plan.md index a060f7429..203afeff3 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。 -状态:todo 184 / doing 0 / blocked 0 / done 30 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 183 / doing 0 / blocked 0 / done 31 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -133,7 +133,7 @@ | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | --- | --- | --- | --- | --- | --- | --- | | PKG-CHAPTER-01 | artplayer-plugin-chapter
核对包契约与历史用法 | BASE-05 | chapters 时间区间、update、name 和进度 DOM 清单 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done | -| PKG-CHAPTER-02 | artplayer-plugin-chapter
建立特有行为与错误测试 | PKG-CHAPTER-01, ENG-03, ENG-05 | 区间重叠/空列表/边界 seek、hover 标题和 update 用例 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言;修正 BASE-TYPE-02 的可选参数声明并补正反例 | M | todo | +| PKG-CHAPTER-02 | artplayer-plugin-chapter
建立特有行为与错误测试 | PKG-CHAPTER-01, ENG-03, ENG-05 | 区间重叠/空列表/边界 seek、hover 标题和 update 用例 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言;修正 BASE-TYPE-02 的可选参数声明并补正反例 | M | done | | PKG-CHAPTER-03 | artplayer-plugin-chapter
整理内部职责与资源 | PKG-CHAPTER-02, ENG-07 | 在已有核心上整理时间区间计算、进度 DOM、事件/布局,合理简单代码可保持 | 不依赖新核心方法或大范围设置重构;旧 API/事件和资源通过,必要适配限包内 | M | todo | | PKG-CHAPTER-04 | artplayer-plugin-chapter
迁移自有源码和公开类型 | PKG-CHAPTER-03, ENG-04, ENG-06 | 基于既有公开核心类型的 Chapters/Option/Result 与 update 推导;JS/TS 工具链试点 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容;处理 BASE-TYPE-01/03,保持运行时和新旧 TS 模块解析消费 | M | todo | | PKG-CHAPTER-05 | artplayer-plugin-chapter
验证新旧核心和组合 | PKG-CHAPTER-04, CORE-22 | chapter + quality/thumbnail、移动和全屏进度场景 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo | @@ -433,3 +433,4 @@ - ENG-05: [记录](changes/2026-09-10-ENG-05-browser-tests.md) [记录](baselines/browser-validation.json) - ENG-06: [记录](changes/2026-09-10-ENG-06-build-development.md) [记录](build-development.md) [记录](baselines/build-validation.json) - PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md) +- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 4593519a4..5c3a93321 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,9 @@ # 进度与证据 +## 当前实施:PKG-CHAPTER-02 测试与声明修正完成 + +chapter 27 项真实浏览器回归通过,合并原 smoke 共 39 项;工厂 option 声明已扩展为可选并重新生成编辑器声明,新旧 TS 正反例通过,BASE-TYPE-02 关闭。见 [交付](changes/2026-09-10-PKG-CHAPTER-02-tests.md)。当前 214 项,31 完成、183 待办。PKG-CHAPTER-01 提交 70174952,本任务单独提交;下一项 ENG-07,随后直接以 TS 拆分 chapter 源码。运行时源码尚未迁移。 + ## 当前实施:PKG-CHAPTER-01 契约完成 源码/发布插件逻辑和元数据对照完成,冻结 chapter 调用、数组变更、DOM/事件和入口,登记待修问题;见 [契约](baselines/chapter-contract.md)。当前 214 项,30 完成、184 待办。ENG-05 提交 7318d338,下一项 PKG-CHAPTER-02 包特有测试与声明修正。生产源码 TS 迁移仍为零。 diff --git a/refactor/risk-guide.md b/refactor/risk-guide.md index e66890c55..cfbbd8c58 100644 --- a/refactor/risk-guide.md +++ b/refactor/risk-guide.md @@ -4,7 +4,7 @@ BASE-07 将先前基线发现和第三方来源缺口纳入同一份责任台账 ## 如何读取状态 -BASE-07 初始登记 39 项,目前仍为 open;ENG-06 新增并修复 BUILD-AMD-01,共 40 项。reproduced 表示已保存复现结果;source-observed 表示在源码、声明或产物中看到事实;unverified 表示来源、环境或行为仍需取证。证据等级与处理状态互相独立,不能把全部事项称为缺陷,也不能因建立了测试基线而自动接受旧缺陷。 +BASE-07 初始登记 39 项;后续新增和关闭状态以 risks.json 为准。ENG-06 修复 BUILD-AMD-01,PKG-CHAPTER-02 修复 BASE-TYPE-02,均有独立回归证据。reproduced 表示已保存复现结果;source-observed 表示在源码、声明或产物中看到事实;unverified 表示来源、环境或行为仍需取证。证据等级与处理状态互相独立,不能把全部事项称为缺陷,也不能因建立了测试基线而自动接受旧缺陷。 其中 15 项来自生命周期、DOM/示例、消费者类型/分发和性能基线;其余包含发布来源、站点分发、素材来源、8 组内嵌资源和 12 个集成边界。每项都列出负责的任务 ID、证据路径、兼容处理路线与关闭条件。具体复现仍以原基线报告为准。 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index cd86465f7..383d87f5d 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -14,7 +14,7 @@ | BASE-DOM-02 | open / 已复现 | 窄容器中控件裁切 | CORE-13, CORE-14 | | BASE-DEMO-01 | open / 源码/产物事实 | 旧 thumbnail 插件示例不对应当前 workspace 包 | SITE-01, EX-03 | | BASE-TYPE-01 | open / 已复现 | NodeNext ESM 默认导出与声明互操作失配 | ENG-04, CORE-07, PKG-CHAPTER-04 | -| BASE-TYPE-02 | open / 已复现 | chapter 可选运行时参数被声明为必填 | PKG-CHAPTER-02 | +| BASE-TYPE-02 | resolved / 已复现 | chapter 可选运行时参数被声明为必填 | PKG-CHAPTER-02 | | BASE-TYPE-03 | open / 已复现 | chapter legacy 缺旧模块解析的声明回退 | ENG-04, PKG-CHAPTER-04 | | BASE-TYPE-04 | open / 源码/产物事实 | plugins.add / toggle 的旧声明返回值与运行时不同 | CORE-07 | | BASE-DIST-01 | open / 源码/产物事实 | thumbnail tool 分发入口与类型文件缺失 | PKG-TOOL-THUMB-01, PKG-TOOL-THUMB-04 | diff --git a/refactor/risks.json b/refactor/risks.json index 9964f2868..75d9c0f7f 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "task": "BASE-07", "sourceCommit": "e01f39ff", - "updatedAt": "2026-09-10T13:46:01.011Z", + "updatedAt": "2026-09-10T14:41:52.219Z", "note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.", "items": [ { @@ -163,7 +163,7 @@ "id": "BASE-TYPE-02", "title": "chapter 可选运行时参数被声明为必填", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-CHAPTER-02" ], @@ -172,7 +172,15 @@ "refactor/fixtures/api.js" ], "compatibleResolution": "补充兼容可选参数,不删除已接受的配置。", - "closureCriteria": "无参数和正确参数编译,错误参数依旧被拒绝,运行时一致。" + "closureCriteria": "无参数和正确参数编译,错误参数依旧被拒绝,运行时一致。", + "resolutionEvidence": [ + "packages/artplayer-plugin-chapter/types/artplayer-plugin-chapter.d.ts", + "test/types/chapter-options.ts", + "refactor/scripts/types.test.mjs", + "test/browser/chapter.spec.js", + "refactor/changes/2026-09-10-PKG-CHAPTER-02-tests.md" + ], + "resolutionRationale": "工厂 option 扩展为可选,旧/新 TS 消费与实际无参插件注册通过;update 及非法输入仍受严格约束。编辑器声明已重新生成。NodeNext ESM 与 legacy 分发问题仍由其他条目跟踪。" }, { "id": "BASE-TYPE-03", diff --git a/refactor/scripts/types.test.mjs b/refactor/scripts/types.test.mjs index d54603901..1374f63cf 100644 --- a/refactor/scripts/types.test.mjs +++ b/refactor/scripts/types.test.mjs @@ -35,3 +35,14 @@ test('Browser implementation configuration catches source errors without emittin fs.rmSync(directory, { recursive: true, force: true }) } }) + +test('Chapter optional factory parameters preserve strict update and data types in new and old compilers', () => { + const source = fs.readFileSync(path.join(refactorDir, '../test/types/chapter-options.ts'), 'utf8') + for (const mode of ['node10-commonjs', 'nodenext-cjs', 'bundler-esm']) { + assert.deepEqual(checkConsumer(ts, mode, source), []) + } + assert.deepEqual(checkConsumer(compat, 'node10-commonjs', source), []) + const unguarded = source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '') + const diagnostics = checkConsumer(ts, 'bundler-esm', unguarded) + assert.equal(diagnostics.length, 3, 'Invalid update, title and null option must each fail') +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index 3ab2a53d5..1a7435422 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1161,11 +1161,14 @@ "ENG-03", "ENG-05" ], - "status": "todo", + "status": "done", "risk": "M", "deliverable": "区间重叠/空列表/边界 seek、hover 标题和 update 用例", "acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言;修正 BASE-TYPE-02 的可选参数声明并补正反例", - "evidence": [] + "evidence": [ + "changes/2026-09-10-PKG-CHAPTER-02-tests.md", + "baselines/chapter-validation.json" + ] }, { "id": "PKG-CHAPTER-03", diff --git a/test/browser/README.md b/test/browser/README.md index 0ea89f9f4..864a20d86 100644 --- a/test/browser/README.md +++ b/test/browser/README.md @@ -23,6 +23,10 @@ failed requests and final state. Tests attach decoded pixels and server-side med Windows WebKit media requests are not reliably visible to Playwright's page network events; use the loopback request log to prove that media was fetched. Its `videoWidth` can reflect layout size, so intrinsic-size compatibility is a separate test, not the playback-success predicate. +Chromium's `net::ERR_ABORTED` and Firefox's `NS_ERROR_PARSED_DATA_CACHED` may cancel media range +requests after buffering. The successful-playback tests allow only the exact engine-specific code, +same-origin media type and two URLs bearing the current test ID; raw failures remain in reports. +See [Mozilla's media-cache explanation](https://bugzilla.mozilla.org/show_bug.cgi?id=1347174#c4). Reports, failure screenshots and traces are under `refactor/.cache/browser/`. They contain local test URLs and paths. JSON and HTML are regenerated each run; archive them before another run diff --git a/test/browser/chapter.spec.js b/test/browser/chapter.spec.js new file mode 100644 index 000000000..639aa3fe2 --- /dev/null +++ b/test/browser/chapter.spec.js @@ -0,0 +1,155 @@ +import { expect, test } from './fixtures.js' + +async function openChapter(page, chapter, chapters = []) { + await page.goto(`/test/player.html?core=published&chapter=${chapter}`) + await page.evaluate((chapters) => { + window.chapterInput = chapters + window.createPlayer('/test/pattern.mp4', { chapters }) + }, chapters) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) +} + +function segments(page) { + return page.locator('.art-chapter').evaluateAll(nodes => nodes.map(node => ({ + start: node.dataset.start, + end: node.dataset.end, + title: node.dataset.title, + width: node.style.width, + }))) +} + +async function hover(page, percentage) { + const box = await page.locator('.art-control-progress-inner').boundingBox() + const x = Math.round(box.x + box.width * percentage) + await page.mouse.move(x, box.y + box.height / 2) + return (x - box.x) / box.width +} + +for (const chapter of ['published', 'candidate']) { + test.describe(`${chapter} chapter on published core`, () => { + test('factory, registration, input normalization and safe title text', async ({ page }) => { + await openChapter(page, chapter, [{ start: 4, end: 6, title: ' Second ' }, { start: 1, end: 2, title: 'First' }]) + expect(await page.evaluate(() => { + const plugin = window.art.plugins.artplayerPluginChapter + return { factory: typeof window.artplayerPluginChapter(), name: plugin.name, keys: Object.keys(plugin).sort(), input: window.chapterInput } + })).toEqual({ + factory: 'function', + name: 'artplayerPluginChapter', + keys: ['name', 'update'], + input: [{ start: 0, end: 1, title: '' }, { start: 1, end: 2, title: 'First' }, { start: 2, end: 4, title: '' }, { start: 4, end: 6, title: ' Second ' }, { start: 6, end: 8, title: '' }], + }) + expect(await segments(page)).toEqual([ + { start: '0', end: '1', title: '', width: '12.5%' }, + { start: '1', end: '2', title: 'First', width: '12.5%' }, + { start: '2', end: '4', title: '', width: '25%' }, + { start: '4', end: '6', title: 'Second', width: '25%' }, + { start: '6', end: '8', title: '', width: '25%' }, + ]) + await hover(page, 0.2) + await expect(page.locator('.art-chapter-title')).toHaveText('First') + await expect(page.locator('.art-chapter-title b')).toHaveCount(0) + await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1) + await page.evaluate(() => { + window.art.destroy() + window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [window.artplayerPluginChapter()] }) + }) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) + await expect(page.locator('.art-chapter')).toHaveCount(0) + }) + + test('progress bars, real hover, title edges and boundary seek', async ({ page }) => { + await openChapter(page, chapter, [{ start: 0, end: 2, title: 'First chapter' }, { start: 2, end: 6, title: 'Middle' }, { start: 6, end: 8, title: 'Last chapter' }]) + for (const type of ['played', 'loaded']) { + const widths = await page.evaluate((type) => { + window.art.emit('setBar', type, 0.375) + return [...document.querySelectorAll(`.art-chapter .art-progress-${type}`)].map(node => node.style.width) + }, type) + expect(widths).toEqual(['100%', '25%', '0px']) + } + const percentage = await hover(page, 0.375) + const widths = await page.locator('.art-chapter .art-progress-hover').evaluateAll(nodes => nodes.map(node => Number.parseFloat(node.style.width))) + for (const [index, expected] of [100, (percentage * 8 - 2) / 4 * 100, 0].entries()) + expect(widths[index]).toBeCloseTo(expected, 1) + const box = await page.locator('.art-control-progress-inner').boundingBox() + for (const [percentage, title] of [[0.01, 'First chapter'], [0.5, 'Middle'], [0.99, 'Last chapter']]) { + await page.mouse.move(box.x + box.width * percentage, box.y + box.height / 2) + await expect(page.locator('.art-chapter-title')).toHaveText(title) + await expect(page.locator('.art-video-player')).toHaveClass(/art-progress-hover/) + const layout = await page.locator('.art-chapter-title').evaluate(title => ({ left: Number.parseFloat(title.style.left), width: title.clientWidth, container: title.parentElement.clientWidth })) + expect(layout.left).toBeGreaterThanOrEqual(0) + expect(layout.left + layout.width).toBeLessThanOrEqual(layout.container) + } + await page.mouse.click(box.x + box.width / 4, box.y + box.height / 2) + await expect.poll(() => page.locator('video').evaluate(video => video.currentTime)).toBeCloseTo(2, 1) + expect(await page.evaluate(() => { + window.art.emit('setBar', 'played', 1) + return [...document.querySelectorAll('.art-chapter .art-progress-played')].map(node => node.style.width) + })).toEqual(['100%', '100%', '100%']) + }) + + test('sync update, Infinity, object identity, empty lists and invalid data', async ({ page }) => { + await openChapter(page, chapter) + await expect(page.locator('.art-chapter')).toHaveCount(0) + expect(await page.evaluate(() => { + const entry = { start: 0, end: Infinity, title: 'All' } + const chapters = [entry] + const result = window.art.plugins.artplayerPluginChapter.update({ chapters }) + return { result, same: chapters[0] === entry, end: entry.end } + })).toEqual({ result: undefined, same: true, end: 8 }) + await expect(page.locator('.art-chapter')).toHaveCount(1) + await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({})) + await expect(page.locator('.art-chapter')).toHaveCount(0) + await expect(page.locator('.art-video-player')).not.toHaveClass(/artplayer-plugin-chapter/) + const cases = [ + [[{ start: '0', end: 2, title: 'Bad' }], 'TypeError', 'Illegal chapter data type'], + [[{ start: 0, end: 2, title: 12 }], 'TypeError', 'Illegal chapter data type'], + [[{ start: -1, end: 2, title: 'Bad' }], 'Error', 'Illegal chapter time point'], + [[{ start: 2, end: 2, title: 'Bad' }], 'Error', 'Illegal chapter time point'], + [[{ start: 0, end: 9, title: 'Bad' }], 'Error', 'Illegal chapter time point'], + [[{ start: 0, end: 4, title: 'A' }, { start: 3, end: 6, title: 'B' }], 'Error', 'Illegal chapter time point'], + ] + for (const [chapters, name, message] of cases) { + expect(await page.evaluate((chapters) => { + const update = window.art.plugins.artplayerPluginChapter.update + update({ chapters: [{ start: 0, end: Infinity, title: 'Valid before failure' }] }) + try { + update({ chapters }) + } + catch (error) { return { name: error.name, message: error.message } } + }, chapters)).toEqual({ name, message }) + await expect(page.locator('.art-chapter')).toHaveCount(0) + } + expect(await page.evaluate(() => { + try { + window.art.plugins.artplayerPluginChapter.update() + } + catch (error) { return error.name } + })).toBe('TypeError') + }) + + test('explicit source update and core-owned destruction', async ({ page }) => { + await openChapter(page, chapter, [{ start: 0, end: 8, title: 'Initial' }]) + await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4')) + expect(await page.locator('video').evaluate(video => video.duration)).toBeGreaterThan(90) + await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: 0, end: Infinity, title: 'New source' }] })) + expect(await page.locator('.art-chapter').evaluate(node => Number(node.dataset.end))).toBeGreaterThan(90) + await page.evaluate(() => window.art.destroy()) + await expect(page.locator('.player')).toBeEmpty() + expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0) + await expect(page.locator('style#artplayer-plugin-chapter')).toHaveCount(1) + }) + }) +} + +test('published-only observations: blank title, NaN and once-only metadata are historical defects', async ({ page }, testInfo) => { + await openChapter(page, 'published', [{ start: 0, end: 2, title: 'Previous' }, { start: 4, end: 8, title: 'Next' }]) + await hover(page, 0.1) + await hover(page, 0.375) + const staleTitle = await page.locator('.art-chapter-title').textContent() + expect(staleTitle).toBe('Previous') + await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4')) + expect(await page.locator('.art-chapter').last().evaluate(node => node.dataset.end)).toBe('8') + await page.evaluate(() => window.art.plugins.artplayerPluginChapter.update({ chapters: [{ start: Number.NaN, end: 3, title: 'Invalid' }] })) + expect(await page.locator('.art-chapter').first().evaluate(node => node.dataset.start)).toBe('NaN') + await testInfo.attach('chapter-known-defects', { body: JSON.stringify({ staleTitle, endAfterSourceChange: 8, acceptsNaN: true }), contentType: 'application/json' }) +}) diff --git a/test/browser/playback.spec.js b/test/browser/playback.spec.js index 4838ed1e4..ef4d1b540 100644 --- a/test/browser/playback.spec.js +++ b/test/browser/playback.spec.js @@ -1,7 +1,7 @@ import { expect, test } from './fixtures.js' for (const core of ['published', 'candidate']) { - test(`${core}: real playback, pause, seek, switch and destroy`, async ({ page, request, diagnostics }, testInfo) => { + test(`${core}: real playback, pause, seek, switch and destroy`, async ({ page, request, diagnostics, browserName }, testInfo) => { const caseId = encodeURIComponent(testInfo.testId) await page.goto(`/test/player.html?core=${core}`) await page.evaluate(url => window.createPlayer(url, true), `/test/pattern.mp4?case=${caseId}`) @@ -55,11 +55,13 @@ for (const core of ['published', 'candidate']) { await expect(page.locator('.player')).toBeEmpty() expect(await page.evaluate(() => window.Artplayer.instances.length)).toBe(0) expect(diagnostics.consoleErrors).toEqual([]) - // Chromium cancels media range requests as buffering, seeking and source ownership change. + // Engines cancel media range requests as buffering, seeking and source ownership change. // Only exact cancellations of this test's two media URLs are expected; retain all in evidence. const unexpectedRequests = diagnostics.failedRequests.filter((item) => { const url = new URL(item.url) - return !(item.resourceType === 'media' && item.failure?.errorText === 'net::ERR_ABORTED' + const expectedCancellation = (browserName === 'chromium' && item.failure?.errorText === 'net::ERR_ABORTED') + || (browserName === 'firefox' && item.failure?.errorText === 'NS_ERROR_PARSED_DATA_CACHED') + return !(item.resourceType === 'media' && expectedCancellation && url.origin === new URL(page.url()).origin && url.searchParams.get('case') === testInfo.testId && ['/test/pattern.mp4', '/assets/sample/video.mp4'].includes(url.pathname)) diff --git a/test/browser/player.html b/test/browser/player.html index 52f0901c0..fd2beab41 100644 --- a/test/browser/player.html +++ b/test/browser/player.html @@ -24,7 +24,11 @@ url, muted: true, plugins: chapter - ? [artplayerPluginChapter({ chapters: [{ start: 0, end: Infinity, title: 'Pattern' }] })] + ? [ + artplayerPluginChapter( + chapter === true ? { chapters: [{ start: 0, end: Infinity, title: 'Pattern' }] } : chapter, + ), + ] : [], }) for (const name of [ diff --git a/test/types/chapter-options.ts b/test/types/chapter-options.ts new file mode 100644 index 000000000..938860ee2 --- /dev/null +++ b/test/types/chapter-options.ts @@ -0,0 +1,16 @@ +import type Artplayer from 'artplayer' +import chapter from 'artplayer-plugin-chapter' + +declare const art: Artplayer +chapter() +chapter(undefined) +chapter({}) +chapter({ chapters: [] })(art).update({}) +const name: 'artplayerPluginChapter' = chapter()(art).name +void name +// @ts-expect-error The update object remains required. +chapter()(art).update() +// @ts-expect-error A chapter title remains a string. +chapter({ chapters: [{ start: 0, end: 1, title: 12 }] }) +// @ts-expect-error null is not an options object. +chapter(null)