test(vtt-thumbnail): [PKG-VTT-THUMB-02] reproduce parsing and lifecycle failures

This commit is contained in:
Harvey Zhao committed 2026-09-13 08:21:48 +08:00
1 parent d4ede650e8
commit 05244d1104
10 files changed
+245 -11

No files matched your search

+3 -2
View File
@@ -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": {
@@ -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"
]
}
@@ -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)。本任务独立本地提交;
回退只移除这批故障测试、命令和记录,不改变播放器运行逻辑。没有推送或发布。
+1 -1
View File
@@ -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 |
+3 -2
View File
@@ -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<br>核对包契约与历史用法 | BASE-05 | vtt/style、异步返回、预览控件名与样式契约 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done |
| PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail<br>建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo |
| PKG-VTT-THUMB-02 | artplayer-plugin-vtt-thumbnail<br>建立特有行为与错误测试 | PKG-VTT-THUMB-01, ENG-03, ENG-05 | VTT 解析、xywh、相对 URL、时间边界、请求失败和定位 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done |
| PKG-VTT-THUMB-03 | artplayer-plugin-vtt-thumbnail<br>整理内部职责与资源 | PKG-VTT-THUMB-02, CORE-09, CORE-19 | 纯解析/区间查找与 DOM、请求/定时器生命周期分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo |
| PKG-VTT-THUMB-04 | artplayer-plugin-vtt-thumbnail<br>迁移自有源码和公开类型 | PKG-VTT-THUMB-03, ENG-04, ENG-06, CORE-07 | cue/rect/result 和参数可选性保持兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo |
| PKG-VTT-THUMB-05 | artplayer-plugin-vtt-thumbnail<br>验证新旧核心和组合 | 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)
+10
View File
@@ -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 项索引/风险检查通过。
+1 -1
View File
@@ -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 |
+7 -3
View File
@@ -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."
+5 -2
View File
@@ -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",
+138
View File
@@ -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`)
}
})