From 05244d1104997bf79148156fa5766dcbf8836293 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 08:21:48 +0800 Subject: [PATCH] test(vtt-thumbnail): [PKG-VTT-THUMB-02] reproduce parsing and lifecycle failures --- package.json | 5 +- .../baselines/vtt-thumbnail-failures.json | 42 ++++++ .../2026-09-13-PKG-VTT-THUMB-02-failures.md | 35 +++++ refactor/environment-matrix.md | 2 +- refactor/plan.md | 5 +- refactor/progress.md | 10 ++ refactor/risk-table.md | 2 +- refactor/risks.json | 10 +- refactor/tasks.json | 7 +- test/vtt-thumbnail.test.js | 138 ++++++++++++++++++ 10 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 refactor/baselines/vtt-thumbnail-failures.json create mode 100644 refactor/changes/2026-09-13-PKG-VTT-THUMB-02-failures.md create mode 100644 test/vtt-thumbnail.test.js diff --git a/package.json b/package.json index 269ba0ba8..5bda74426 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", + "test:unit": "node --test test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -85,7 +85,8 @@ "test:iframe-types-package": "node refactor/scripts/iframe-package-types.mjs", "test:thumbnail": "node --test test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js refactor/scripts/thumbnail-contract.test.mjs refactor/scripts/thumbnail-types.test.mjs", "test:thumbnail-types-package": "node refactor/scripts/thumbnail-package-types.mjs", - "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs" + "test:auto-thumbnail": "node --test test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js refactor/scripts/auto-thumbnail-contract.test.mjs", + "test:vtt-thumbnail": "node --test test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/refactor/baselines/vtt-thumbnail-failures.json b/refactor/baselines/vtt-thumbnail-failures.json new file mode 100644 index 000000000..58ced9daa --- /dev/null +++ b/refactor/baselines/vtt-thumbnail-failures.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "task": "PKG-VTT-THUMB-02", + "status": "done", + "parentCommit": "d4ede650e8188dab3c21524cf2e524887412a436", + "checks": [ + { + "command": "node --test test/vtt-thumbnail.test.js", + "result": "pass", + "tests": 108, + "log": "refactor/.cache/vtt-thumbnail02-initial.log" + }, + { + "command": "yarn test:vtt-thumbnail", + "result": "pass", + "tests": 199, + "log": "refactor/.cache/vtt-thumbnail02-targeted.log" + }, + { + "command": "yarn ci:check", + "result": "pass", + "tests": { + "unit": 1680, + "engineering": 14, + "baseline": 323, + "total": 2017 + }, + "log": "refactor/.cache/vtt-thumbnail02-ci.log", + "logSha256": "1d478933a55cde3b947b5a20a6dc7f1d346729a8afaa6adf5075365f7fcd0643" + } + ], + "fixture": { + "file": "docs/assets/sample/bbb-thumbnails.vtt", + "sha256": "1b88c30cf67d5c3da8281a150e8277858781c8077df0c6bfd2f32550da7323ba", + "cues": 120 + }, + "limitations": [ + "Historical failures are reproduced, not fixed", + "Controlled host only; no native browser, installed package or final core claim", + "Twelve fixtures preserve actual archive/source distinctions from task 01" + ] +} diff --git a/refactor/changes/2026-09-13-PKG-VTT-THUMB-02-failures.md b/refactor/changes/2026-09-13-PKG-VTT-THUMB-02-failures.md new file mode 100644 index 000000000..ff4f2fd07 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-VTT-THUMB-02-failures.md @@ -0,0 +1,35 @@ +# PKG-VTT-THUMB-02 异常与生命周期复现 + +新增 test/vtt-thumbnail.test.js,108 项历史测试全部通过。这里的通过表示 +**成功复现历史结果**,不表示候选已经修复。十二个固定夹具复用 01 的归档/源码校验; +1.0.0 仅有效随包源码,1.0.1–1.0.3 编译产物显式使用其可接受的 compact 时间戳。 + +## 证据与处置 + +| 场景 | 真实旧代码在可控 host 中的结果 | 03 的处置方向 | +| --- | --- | --- | +| fetch 尚未完成即 destroy | 没有请求选项或取消信号,返回后仍添加控件并响应 hover | 注册前建立生命周期,阻止迟到安装并处理取消 | +| 移动拖动后 destroy | 500ms 计时器和 setBar 仍保留;迟到回调继续改样式 | 所有权覆盖监听器、计时器和控件,阻断销毁后写入 | +| fetch 或 response.text 拒绝 | 原始异常通过注册 Promise 拒绝,无控件/计时器 | 保持原始拒绝,并释放新增生命周期资源 | +| HTTP 404 但 body 为有效 VTT | 未检查 ok,仍显示图片 | 后续显式拒绝 HTTP 失败,作为缺陷修复记录 | +| 空文本或仅 WEBVTT | 添加空控件,hover 隐藏 | 保留有效空结果,避免误判为请求异常 | +| 错误时间、无 payload、无 hash、cue ID、NOTE | TypeError,安装之前拒绝 | 分开有效标准扩展与格式损坏,给出可定位错误,不静默写入坏 CSS | +| 重排 yxhw、坏坐标、缺坐标 | 字符串按键映射;坏值直接形成 CSS | 正常几何保持,异常输入补明确检查,不能把重排键当成不存在 | +| restart 后修改 option.vtt | 不重新请求,继续使用注册时的 cue 列表 | 不能未经公开行为评估新增自动重新下载 | +| cue 间隙、NaN 进度 | 隐藏预览,不选择另一个 cue | 保持边界行为 | + +另用真实 docs/assets/sample/bbb-thumbnails.vtt 的 **120 个 cue**逐个触发命中, +核对图片路径、x/y 背景位置、宽高。该文件 SHA 与运行结果在验证记录中; +这仍是受控样式记录,不等于浏览器 CSS 接受了这些值或真实图片已加载。 + +复现不修改候选生产源码、声明或 dist,也不增加依赖。新增 `yarn test:vtt-thumbnail` +用于运行本包历史契约和故障测试;故障测试同时纳入 `test:unit`,CI 会持续执行。 +VTT-THUMB-LIFE-01 从 source-observed 提升为 reproduced,所有风险继续 open。 + +## 后续任务与回退 + +03 拆分纯解析/查找、请求、视图和注册资源,添加面向修复结果的候选测试,先在旧版上 +观察失败再验证候选;当前历史断言永久保留为对照,不能直接改成新行为消除差异。 +04 严格 TS 和声明消费者;05 真实核心/浏览器,06 构建安装包与 demo/编辑器。 +执行结果见 [验证记录](../baselines/vtt-thumbnail-failures.json)。本任务独立本地提交; +回退只移除这批故障测试、命令和记录,不改变播放器运行逻辑。没有推送或发布。 diff --git a/refactor/environment-matrix.md b/refactor/environment-matrix.md index 95256bfbb..ff3415673 100644 --- a/refactor/environment-matrix.md +++ b/refactor/environment-matrix.md @@ -57,7 +57,7 @@ Node10 是 TS 模块解析模式名,不代表支持 Node.js 10。现代 es2020 | artplayer-plugin-jassub / 1.1.0 | ASS、worker/WASM/font 加载、切源/销毁;短 ASS+明确来源字体 | 内嵌资源哈希已有;版本对应、跨源/worker 限制、实际渲染待 | PKG-JASSUB-01/02/05/06 | | artplayer-plugin-multiple-subtitles / 1.2.0 | VTT 合并/排序/解析错误、URL 回收;双语言/空/坏 VTT | parser 来源头已观察;特有行为和浏览器字幕待 | PKG-MULTI-SUB-01/02/05/06 | | artplayer-plugin-vast / 1.2.0 | Glomex/Google IMA、广告错误/恢复、重复初始化;供应商测试广告标签 | 依赖版本已记录;实际 IMA 网络/广告流程待,PR 用受控 adapter | PKG-VAST-01/02/05/06 | -| artplayer-plugin-vtt-thumbnail / 1.1.0 | WebVTT 时间和 sprite xywh、图片失败/跨源;本地 VTT+sprite | 发布/浏览器/边界待 | PKG-VTT-THUMB-01/02/05/06 | +| artplayer-plugin-vtt-thumbnail / 1.1.0 | WebVTT 时间和 sprite xywh、图片失败/跨源;本地 VTT+sprite | 五个实际发布归档和可控历史边界/故障已核对;原生图片、浏览器、核心组合仍待 05/06 | PKG-VTT-THUMB-01/02/05/06 | | artplayer-proxy-canvas / 1.1.0 | video-like 属性/事件、Canvas 绘制、seek/销毁;MP4 | shim 与实际媒体顺序、跨源/移动待 | PKG-CANVAS-01/02/05/06 | | artplayer-proxy-mediabunny / 1.2.0 | Mediabunny 1.56.1 当前解析;WebCodecs/AudioContext、HLS/轨道拓扑、AV 同步 | 库依赖不等于 codec 可用;准确 codec/profile、设备能力、长播放待 | PKG-MB-01/02/07/08/09/10 | | artplayer-tool-iframe / 1.1.0 | 跨窗协议、ready/错误/销毁;同源与两个受控不同 origin 页面 | 消息/嵌套浏览器及沙箱权限待 | PKG-IFRAME-01/02/05/06 | diff --git a/refactor/plan.md b/refactor/plan.md index f59dddd89..fe5ac80b3 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 95 / doing 12 / blocked 0 / done 115 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 94 / doing 12 / blocked 0 / done 116 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -183,7 +183,7 @@ | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | --- | --- | --- | --- | --- | --- | --- | | PKG-VTT-THUMB-01 | artplayer-plugin-vtt-thumbnail
核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done | -| PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail
建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo | +| PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail
建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done | | PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail
整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo | | PKG-VTT-THUMB-04 | artplayer-plugin-vtt-thumbnail
迁移自有源码和公开类型 | PKG-VTT-THUMB-03, ENG-04, ENG-06, CORE-07 | cue/rect/result 和参数可选性保持兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo | | PKG-VTT-THUMB-05 | artplayer-plugin-vtt-thumbnail
验证新旧核心和组合 | PKG-VTT-THUMB-04, CORE-22 | 鼠标和移动进度、全屏、切源、chapter 组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo | @@ -498,6 +498,7 @@ - PKG-AUTO-THUMB-02: [记录](baselines/auto-thumbnail-failures.md) [记录](baselines/auto-thumbnail-failures-validation.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-02-failures.md) - PKG-AUTO-THUMB-03: [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-lifecycle-checkpoint.md) [记录](baselines/auto-thumbnail-lifecycle-checkpoint.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-hidden-renderer.md) [记录](baselines/auto-thumbnail-hidden-renderer.json) [记录](baselines/auto-thumbnail-timeline-media.json) [记录](changes/2026-09-13-PKG-AUTO-THUMB-03-frame-presentation.md) [记录](baselines/auto-thumbnail-frame-presentation.json) - PKG-VTT-THUMB-01: [记录](baselines/vtt-thumbnail-release.json) [记录](baselines/vtt-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-VTT-THUMB-01-contract.md) +- PKG-VTT-THUMB-02: [记录](baselines/vtt-thumbnail-failures.json) [记录](changes/2026-09-13-PKG-VTT-THUMB-02-failures.md) - PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md) - PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md) - PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md) diff --git a/refactor/progress.md b/refactor/progress.md index 9908038c8..aedb2a6b9 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,15 @@ # 进度与证据 +## PKG-VTT-THUMB-02 特有失败与生命周期复现完成 + +108 项新历史测试复现销毁后安装/计时器写入、请求拒绝、HTTP 状态忽略、非法 VTT/坐标、 +切源不重载和边界;真实 demo 的 120 cue 坐标逐条核对。包命令共 199 项通过, +完整 CI 2017 项(1680 单元、14 工程、323 基线)通过。历史复现不是候选修复, +原生图片/核心组合仍待 05/06;见[变更](changes/2026-09-13-PKG-VTT-THUMB-02-failures.md)和 +[验证](baselines/vtt-thumbnail-failures.json)。下一步 03 拆分并修复资源所有权,04 严格 TS。 +222 项:116 done、12 doing、94 todo;独立本地提交,不推送、不发布。 + + ## PKG-VTT-THUMB-01 历史契约完成 五份真实 npm 归档、34 成员、十份 Git 输入已冻结,91 项包契约及 9 项索引/风险检查通过。 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 7cbcbb493..1820d1246 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -191,6 +191,6 @@ | AUTO-THUMB-PIXEL-01 | open / 已复现 | Historical auto-thumbnail yields black sampled JPEG cells on Windows WebKit while played native video and static JPEG controls yield color | PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 | | VTT-THUMB-DIST-01 | open / 已复现 | Historical 1.0.0 main/legacy contain invalid regular expressions and cannot load | PKG-VTT-THUMB-06 | | VTT-THUMB-PARSE-01 | open / 已复现 | Historical compiled arrows differ from shipped source; parsing floors bounds and assumes alternating lines | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03 | -| VTT-THUMB-LIFE-01 | open / 源码/产物事实 | Fetch completion, setBar listeners and mobile timers have no destroy ownership | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-05 | +| VTT-THUMB-LIFE-01 | open / 已复现 | Fetch completion, setBar listeners and mobile timers have no destroy ownership | PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-05 | | VTT-THUMB-TYPE-01 | open / 已复现 | Published declarations falsely describe async registration as a synchronous result | PKG-VTT-THUMB-04 | | VTT-THUMB-EXPORT-01 | open / 已复现 | Older CommonJS default objects and thumbnails control names differ from latest direct export and vtt-thumbnail name | PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 | diff --git a/refactor/risks.json b/refactor/risks.json index e3ca481d5..85118ae04 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4149,7 +4149,9 @@ "evidence": [ "refactor/baselines/vtt-thumbnail-release.json", "refactor/baselines/vtt-thumbnail-contract.md", - "refactor/scripts/vtt-thumbnail-contract.test.mjs" + "refactor/scripts/vtt-thumbnail-contract.test.mjs", + "test/vtt-thumbnail.test.js", + "refactor/baselines/vtt-thumbnail-failures.json" ], "compatibleResolution": "Preserve valid cue selection and geometry; test malformed data and standard VTT separately from old compiler defects.", "closureCriteria": "Parser fixtures include malformed, empty, real demo VTT, fractional and shared bounds, relative URLs and historical accepted formats." @@ -4157,7 +4159,7 @@ { "id": "VTT-THUMB-LIFE-01", "title": "Fetch completion, setBar listeners and mobile timers have no destroy ownership", - "confirmation": "source-observed", + "confirmation": "reproduced", "status": "open", "owners": [ "PKG-VTT-THUMB-02", @@ -4167,7 +4169,9 @@ "evidence": [ "refactor/baselines/vtt-thumbnail-release.json", "refactor/baselines/vtt-thumbnail-contract.md", - "refactor/scripts/vtt-thumbnail-contract.test.mjs" + "refactor/scripts/vtt-thumbnail-contract.test.mjs", + "test/vtt-thumbnail.test.js", + "refactor/baselines/vtt-thumbnail-failures.json" ], "compatibleResolution": "Own requests, listeners, timers and controls per registration without stale writes or cross-instance cleanup.", "closureCriteria": "Deferred request, destroy, failure, timer, repeated registration and native core scenarios demonstrate cleanup." diff --git a/refactor/tasks.json b/refactor/tasks.json index 931af6e15..4bfadafd7 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1876,11 +1876,14 @@ "ENG-03", "ENG-05" ], - "status": "todo", + "status": "done", "risk": "M", "deliverable": "VTT 解析、xywh、相对 URL、时间边界、请求失败和定位", "acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言", - "evidence": [] + "evidence": [ + "baselines/vtt-thumbnail-failures.json", + "changes/2026-09-13-PKG-VTT-THUMB-02-failures.md" + ] }, { "id": "PKG-VTT-THUMB-03", diff --git a/test/vtt-thumbnail.test.js b/test/vtt-thumbnail.test.js new file mode 100644 index 000000000..5733f01d2 --- /dev/null +++ b/test/vtt-thumbnail.test.js @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Frozen historical failure characterization. +import test from 'node:test' +import { acceptedVttText, vttText, vttThumbnailEnvironment, vttThumbnailHistorical } from './helpers/vtt-thumbnail.js' + +for (const implementation of await vttThumbnailHistorical()) { + const text = acceptedVttText(implementation) + test(`VTT-thumbnail ${implementation.name} historical: pending fetch creates controls after destroy without cancellation`, async () => { + const env = vttThumbnailEnvironment(implementation, { text, deferred: true }) + const pending = env.factory({ vtt: '/old.vtt' })(env.art) + await env.emit('destroy') + assert.equal(env.requests[0].length, 1, 'No AbortSignal or request options') + env.resolve() + assert.equal((await pending).name, 'artplayerPluginVttThumbnail') + assert.equal(env.controls.length, 1) + await env.hover(0.3) + assert.equal(env.styles.backgroundImage, 'url(/sheet.jpg)') + assert.equal(env.listeners.get('destroy'), undefined) + }) + + test(`VTT-thumbnail ${implementation.name} historical: network and response-body failures preserve the original rejection`, async () => { + for (const phase of ['fetch', 'text']) { + const env = vttThumbnailEnvironment(implementation, { text, deferred: phase === 'fetch' }) + const failure = new Error(`${phase} failed`) + if (phase === 'text') + env.box.fetch = async () => ({ text: async () => { throw failure } }) + const pending = env.factory({ vtt: '/cues.vtt' })(env.art) + if (phase === 'fetch') + env.reject(failure) + await assert.rejects(pending, error => error === failure) + assert.equal(env.controls.length, 0) + assert.equal(env.listeners.size, 0) + assert.equal(env.timers.size, 0) + } + }) + + test(`VTT-thumbnail ${implementation.name} historical: HTTP failure status is ignored when its body parses`, async () => { + const env = vttThumbnailEnvironment(implementation) + env.box.fetch = async () => ({ ok: false, status: 404, text: async () => text }) + await env.factory({ vtt: '/missing.vtt' })(env.art) + await env.hover(0.3) + assert.equal(env.styles.backgroundImage, 'url(/sheet.jpg)') + }) + + test(`VTT-thumbnail ${implementation.name} historical: empty and header-only responses create an empty hidden preview`, async () => { + for (const source of ['', 'WEBVTT\r\n\r\n']) { + const env = vttThumbnailEnvironment(implementation, { text: source }) + await env.factory({ vtt: '/empty.vtt' })(env.art) + assert.equal(env.controls.length, 1) + await env.hover(0.3) + assert.equal(env.styles.display, 'none') + } + }) + + test(`VTT-thumbnail ${implementation.name} historical: unmatched timestamps, missing payload, cue IDs and NOTE reject before mounting`, async () => { + for (const source of [ + 'WEBVTT\n\nnot a timestamp\nsheet.jpg#xywh=0,0,80,45', + 'WEBVTT\n\n00:00.000 --> 00:05.000', + 'WEBVTT\n\n00:00.000 --> 00:05.000\nsheet.jpg', + 'WEBVTT\n\ncue-id\n00:00.000 --> 00:05.000\nsheet.jpg#xywh=0,0,80,45', + 'WEBVTT\n\nNOTE comment\n\n00:00.000 --> 00:05.000\nsheet.jpg#xywh=0,0,80,45', + ]) { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation, source) }) + await assert.rejects(env.factory({ vtt: '/invalid.vtt' })(env.art), error => error.name === 'TypeError') + assert.equal(env.controls.length, 0) + } + }) + + test(`VTT-thumbnail ${implementation.name} historical: raw reordered xywh keys and malformed coordinate values reach styling`, async () => { + for (const [coordinates, expected] of [ + ['yxhw=20,10,45,80', { width: '80px', height: '45px', backgroundPosition: '-10px -20px' }], + ['xywh=bad,-2,80', { width: '80px', height: 'undefinedpx', backgroundPosition: '-badpx --2px' }], + ]) { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation, `WEBVTT\n\n00:00.000 --> 00:10.000\nsheet.jpg#${coordinates}`) }) + await env.factory({ vtt: '/cues.vtt' })(env.art) + await env.hover(0.3) + for (const [key, value] of Object.entries(expected)) assert.equal(env.styles[key], value) + } + }) + + test(`VTT-thumbnail ${implementation.name} historical: restart retains the original cue list without another request`, async () => { + const env = vttThumbnailEnvironment(implementation, { text }) + const options = { vtt: '/old/cues.vtt' } + await env.factory(options)(env.art) + options.vtt = '/new/cues.vtt' + await env.emit('restart', '/new-video.mp4') + await env.hover(0.7) + assert.equal(env.requests.length, 1) + assert.equal(env.styles.backgroundImage, 'url(/old/second.jpg)') + }) + + test(`VTT-thumbnail ${implementation.name} historical: gaps and non-finite percentages hide without selecting another cue`, async () => { + const env = vttThumbnailEnvironment(implementation, { text: acceptedVttText(implementation, vttText.replace('00:00:05.100', '00:00:07.100')) }) + await env.factory({ vtt: '/cues.vtt' })(env.art) + await env.hover(0.3) + assert.equal(env.styles.backgroundImage, 'url(/sheet.jpg)') + await env.hover(0.6) + assert.equal(env.styles.display, 'none') + await env.hover(Number.NaN) + assert.equal(env.styles.display, 'none') + }) + + if (implementation.profile !== 'mouse-source-only') { + test(`VTT-thumbnail ${implementation.name} historical: destroy leaves the mobile timer and setBar callback active`, async () => { + const env = vttThumbnailEnvironment(implementation, { text, mobile: true }) + await env.factory({ vtt: '/cues.vtt' })(env.art) + await env.hover(0.3, 'played', {}) + const timer = [...env.timers.values()][0] + await env.emit('destroy') + assert.equal(env.timers.size, 1) + assert.equal(env.listeners.get('setBar').length, 1) + env.styles.display = 'sentinel' + timer.callback() + assert.equal(env.styles.display, 'none', 'A stale callback still writes the old element') + await env.hover(0.7) + assert.equal(env.styles.backgroundImage, 'url(/second.jpg)') + }) + } +} + +test('VTT-thumbnail frozen current implementation parses every cue in the real local demo VTT', async () => { + const implementation = (await vttThumbnailHistorical()).find(item => item.name === 'frozen-workspace-source') + const text = fs.readFileSync('docs/assets/sample/bbb-thumbnails.vtt', 'utf8') + const rows = text.match(/bbb-sprite\.jpg#xywh=[^\r\n]+/g) + assert(rows.length > 100) + const env = vttThumbnailEnvironment(implementation, { text }) + await env.factory({ vtt: '/assets/sample/bbb-thumbnails.vtt' })(env.art) + env.art.duration = rows.length * 5 + for (const [index, row] of rows.entries()) { + await env.hover((index * 5 + 2.5) / env.art.duration) + const [x, y, width, height] = row.split('=')[1].split(',') + assert.equal(env.styles.backgroundImage, 'url(/assets/sample/bbb-sprite.jpg)') + assert.equal(env.styles.backgroundPosition, `-${x}px -${y}px`) + assert.equal(env.styles.width, `${width}px`) + assert.equal(env.styles.height, `${height}px`) + } +})