test(chapter): PKG-CHAPTER-02 cover behavior and optional factory types

This commit is contained in:
Harvey Zhao committed 2026-09-10 22:43:01 +08:00
1 parent 701749520b
commit 12b047f9a6
17 files changed
+624 -15

No files matched your search

+1 -1
View File
@@ -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
@@ -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
@@ -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
+329
View File
@@ -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": []
}
]
}
@@ -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 继续完成公开类型/分发消费闭环。
回退本提交恢复原必填声明及编辑器输出,并移除新增测试/文档;旧合法调用和运行时代码不变。
+3 -2
View File
@@ -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<br>核对包契约与历史用法 | BASE-05 | chapters 时间区间、update、name 和进度 DOM 清单 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done |
| PKG-CHAPTER-02 | artplayer-plugin-chapter<br>建立特有行为与错误测试 | PKG-CHAPTER-01, ENG-03, ENG-05 | 区间重叠/空列表/边界 seek、hover 标题和 update 用例 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言;修正 BASE-TYPE-02 的可选参数声明并补正反例 | M | todo |
| PKG-CHAPTER-02 | artplayer-plugin-chapter<br>建立特有行为与错误测试 | PKG-CHAPTER-01, ENG-03, ENG-05 | 区间重叠/空列表/边界 seek、hover 标题和 update 用例 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言;修正 BASE-TYPE-02 的可选参数声明并补正反例 | M | done |
| PKG-CHAPTER-03 | artplayer-plugin-chapter<br>整理内部职责与资源 | PKG-CHAPTER-02, ENG-07 | 在已有核心上整理时间区间计算、进度 DOM、事件/布局,合理简单代码可保持 | 不依赖新核心方法或大范围设置重构;旧 API/事件和资源通过,必要适配限包内 | M | todo |
| PKG-CHAPTER-04 | artplayer-plugin-chapter<br>迁移自有源码和公开类型 | 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<br>验证新旧核心和组合 | 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)
+4
View File
@@ -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 迁移仍为零。
+1 -1
View File
@@ -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、证据路径、兼容处理路线与关闭条件。具体复现仍以原基线报告为准。
+1 -1
View File
@@ -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 |
+11 -3
View File
@@ -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",
+11
View File
@@ -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')
})
+5 -2
View File
@@ -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",
+4
View File
@@ -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
+155
View File
@@ -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: '<b>First</b>' }])
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: '<b>First</b>' }, { 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: '<b>First</b>', 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('<b>First</b>')
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' })
})
+5 -3
View File
@@ -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))
+5 -1
View File
@@ -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 [
+16
View File
@@ -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)