mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 09:36:16 -08:00
test(multiple-subtitles): [PKG-MULTI-SUB-02] reproduce failures and native resource leaks
This commit is contained in:
1 parent
5a0d94cb89
commit
36cd151095
12 files changed
+5399
-19
No files matched your search
+2
-2
@@ -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/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js 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:unit": "node --test test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js 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",
|
||||
@@ -88,7 +88,7 @@
|
||||
"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-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js refactor/scripts/vtt-thumbnail-contract.test.mjs refactor/scripts/vtt-thumbnail-types.test.mjs",
|
||||
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -31,6 +31,13 @@ sets the live subtitle option's escape to false and calls subtitle.init with the
|
||||
configuration plus URL/type/onVttLoad overrides. The latest URL and pending requests have
|
||||
no destroy owner yet. These are explicit future fixes, not behavior to silently call safe.
|
||||
|
||||
The failure baseline now reproduces late fetch/body installation, retained and newly allocated
|
||||
URLs after destroy, ignored HTTP failures, orphaned URLs after init failures, and revoking the
|
||||
old URL before a new allocation can fail. Async init rejection is currently unobserved by the
|
||||
plugin. Task 03 must handle these internal failures while retaining the void selection methods.
|
||||
Keep the current parser's best-effort behavior separate from these resource fixes: invalid-header
|
||||
diagnostics can coexist with usable cues, and metadata is intentionally read after download.
|
||||
|
||||
## Vendor boundary and validation
|
||||
|
||||
The comparison revision is w3c/webvtt.js `380cfcce34ba8b472d3a31474874eb72a0e5f460`.
|
||||
@@ -43,6 +50,7 @@ Use the pinned Node and Yarn 1.22.22:
|
||||
|
||||
```sh
|
||||
yarn test:multiple-subtitles
|
||||
yarn test:browser test/browser/multiple-subtitles-history.spec.js
|
||||
yarn build artplayer-plugin-multiple-subtitles
|
||||
```
|
||||
|
||||
@@ -50,6 +58,10 @@ Historical tests read verified npm bytes and frozen Git inputs. For current norm
|
||||
set ARTPLAYER_MULTIPLE_SUBTITLES_CANDIDATE=1; optionally set
|
||||
ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT to an actual main/legacy file, then run
|
||||
`node --test test/multiple-subtitles.test.js`. These tests use controlled subtitle hosts;
|
||||
they do not prove native rendering, decoder coverage or the online editor. Tasks 02 and
|
||||
05/06 own error/lifecycle reproduction, real core/device combinations and demo acceptance.
|
||||
they do not prove native rendering or the online editor. The separate historical browser suite
|
||||
uses actual 1.2.0 main with published/candidate cores: SRT display and selection, pending native
|
||||
fetch after destroy, and native Blob readability/late reset allocations. It covers Chromium,
|
||||
Firefox and Windows WebKit; it is not all historical cores, ASS display or physical Safari.
|
||||
The failure unit suite uses real current core converters and TextDecoder for SRT/ASS/encodings.
|
||||
Tasks 05/06 still own complete core/device combinations and demo acceptance.
|
||||
See `../../refactor/baselines/multiple-subtitles-contract.md` for exact historical differences.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,62 @@
|
||||
# PKG-MULTI-SUB-02 错误与资源生命周期复现
|
||||
|
||||
在 01 的 9 份冻结实现上增加 18 类用例,共 162 项。测试读取真实 npm main/legacy
|
||||
及固定 Git 源码/产物,预期内容明确描述旧行为,不将这些通过结果解释成候选已修复。
|
||||
测试辅助宿主增加原始字节响应、独立 fetch/body 挂起、错误和真实活动 URL 计数;
|
||||
既有正常契约继续检查,未改生产源码、公开声明、parser 或构建产物。
|
||||
|
||||
## 已复现的正常与边界行为
|
||||
|
||||
- 使用当前核心的真实 getExt/srtToVtt/assToVtt 及原生 TextDecoder,验证 SRT/ASS、URL
|
||||
query、大写扩展名、Windows-1252 和 UTF-16LE。不是将 converter stub 当作解码验收。
|
||||
- 请求并行发出,返回先后不会改变轨序;名称等元数据在下载后读取,restart 不重复下载。
|
||||
- 同名轨选择第一个匹配;未知名称抛 TypeError,保留原活动 URL,随后 reset 可以恢复。
|
||||
- 各轨内部排序,但 1.1.0 起合并后不再全局排序;重叠 cue 保留各自时间。
|
||||
- 1.0.0 遇到较短后轨会抛异常,较长后轨的尾 cue 会丢失;后续版本连接所有 cue。
|
||||
- 空轨/未知格式产生空 VTT;parser 的诊断没有向外传递,坏 header 后仍可保留可用 cue。
|
||||
这是旧 parser 的容错观察,不能未经评估就统一改成严格拒绝。
|
||||
|
||||
## 已复现的问题
|
||||
|
||||
| 问题 | 旧行为 | 03 应处理的边界 |
|
||||
| --- | --- | --- |
|
||||
| 请求失败 | 原异常拒绝,但其他请求继续读取 body,没有共享取消 | 保留异常身份并回收同一注册的挂起请求 |
|
||||
| HTTP 状态 | 404 配合可解析 body 仍安装字幕 | 显式处理原生 HTTP 失败,避免错误页面成为字幕 |
|
||||
| destroy 中 fetch/body | Promise 不因 destroy 结束,响应到达后继续 init 和创建 URL | 区分两段异步边界与过期结果 |
|
||||
| destroy 后方法 | 最后 URL 不释放,tracks/reset 仍创建新 URL 和调用 init | 关闭后不再写入或分配资源 |
|
||||
| 同步 init 失败 | 原异常拒绝,但新 Blob URL 泄漏,escape 已修改 | 安装失败回滚和资源所有权 |
|
||||
| 异步 init 失败 | 插件忽略实际返回的 Promise,注册仍成功 | 观察内部拒绝,同时保护 tracks/reset 的旧 void 接口 |
|
||||
| URL 分配失败 | 先 revoke 旧 URL,随后分配失败,宿主仍指向失效资源 | 新旧资源的替换顺序与失败恢复 |
|
||||
|
||||
异步 init 失败用例由测试宿主观察真实 rejection,防止 Node 未处理拒绝结束测试;
|
||||
没有把假设的 future handler 当作旧插件已有能力。浏览器测试也仅记录真实 init 返回值和
|
||||
拒绝,不替换其实现;本次真实环境中的 init 错误数组为空。
|
||||
|
||||
## 原生浏览器证据
|
||||
|
||||
用真实 1.2.0 main 插件与发布/候选核心,在 Chromium、Firefox、Windows WebKit 上执行
|
||||
三场景,共 18 项。核心和插件指纹、浏览器版本、请求/错误记录及观察数据见
|
||||
[验证记录](../baselines/multiple-subtitles-failures.json)。三个场景为:
|
||||
|
||||
1. 真实 SRT 转换、原生 textTrack cue、播放时间推进和 DOM 字幕文本,切换/清空/reset。
|
||||
2. 原生 fetch 挂起期间 destroy:没有 AbortSignal、没有及时结束,放行后在已销毁宿主
|
||||
调用 init;真实生成的 Blob URL 仍能被原生 fetch 读取。
|
||||
3. 正常加载后 destroy:插件 URL 仍可读取;保留的 reset 随后生成另一可读取 URL。
|
||||
|
||||
成功显示场景通过真实 plugins.add;资源观察场景直接执行公开注册函数,以区分插件
|
||||
本身行为与核心插件管理器的结束保护。此证据不覆盖旧支持范围全部核心、ASS 原生显示、
|
||||
实体 Safari/移动设备、全部样式组合或在线编辑器;这些仍归 05/06。
|
||||
|
||||
## 验证和接续
|
||||
|
||||
本包命令新增失败文件,并接入主 test:unit。helper/用例显式 lint;最终完整 CI 和准确
|
||||
统计写入验证记录。包内维护文档同步。没有新依赖,不重建未改变的生产产物。
|
||||
LIFE 风险从源码观察提升为已复现;HTTP/宿主 Promise 问题单独登记,全部保持 open。
|
||||
|
||||
最终本包 215 项、原生浏览器 18 项通过;完整 CI 2285 项通过(1937 单元、14 工程、
|
||||
334 基线),严格生产 TS 仍 346 文件。任务状态更新后另跑 9 项风险/契约元数据测试通过。
|
||||
声明字节与上次指纹的差异经 Git 对照确认仅为 CRLF;验证记录同时保留工作区和 Git 指纹。
|
||||
全量 lint 保留既有生成声明的 unused-disable 警告;新增测试文件的显式 lint 无警告。
|
||||
|
||||
本任务独立本地提交,不推送、不发布。回退只移除新增复现和辅助宿主能力,生产行为不变。
|
||||
接下来 03 拆清获取/转换、纯 merge、注册与资源所有权,保持冻结契约并增加候选修复断言。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 91 / doing 13 / blocked 0 / done 118 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 90 / doing 13 / blocked 0 / done 119 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-MULTI-SUB-01 | artplayer-plugin-multiple-subtitles<br>核对包契约与历史用法 | BASE-05 | subtitles/onParser、multipleSubtitles 名称及实际返回方法 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-03,定位 WebVTT parser 来源/修订及 CC0 头,保留解析/序列化行为 | H | done |
|
||||
| PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles<br>建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
|
||||
| PKG-MULTI-SUB-02 | artplayer-plugin-multiple-subtitles<br>建立特有行为与错误测试 | PKG-MULTI-SUB-01, ENG-03, ENG-05 | VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-MULTI-SUB-03 | artplayer-plugin-multiple-subtitles<br>整理内部职责与资源 | PKG-MULTI-SUB-02, CORE-15 | 解析与合并/获取/渲染分离,过期请求及对象 URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-MULTI-SUB-04 | artplayer-plugin-multiple-subtitles<br>迁移自有源码和公开类型 | PKG-MULTI-SUB-03, ENG-04, ENG-06, CORE-07 | 准确 parser/cue/tree/result 类型与真实异步形状 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-MULTI-SUB-05 | artplayer-plugin-multiple-subtitles<br>验证新旧核心和组合 | PKG-MULTI-SUB-04, CORE-22 | 与核心字幕/偏移/全屏组合,旧名称调用不变 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
@@ -512,6 +512,7 @@
|
||||
- PKG-DASH-04: [记录](changes/2026-09-12-PKG-DASH-04-types.md) [记录](baselines/dash-types-validation.json) [记录](dash-validation.md)
|
||||
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json)
|
||||
- PKG-MULTI-SUB-01: [记录](baselines/multiple-subtitles-release.json) [记录](baselines/multiple-subtitles-contract.md) [记录](baselines/multiple-subtitles-contract-validation.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-01-contract.md)
|
||||
- PKG-MULTI-SUB-02: [记录](baselines/multiple-subtitles-failures.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md)
|
||||
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
|
||||
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
|
||||
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MULTI-SUB-02 错误与生命周期复现完成
|
||||
|
||||
9 份历史实现各 18 类异常/边界,共 162 项;本包命令及主 test:unit 已接入,
|
||||
本包 215 项通过。真实核心 converter 与 TextDecoder 覆盖 SRT/ASS/编码,另验证
|
||||
轨序、重叠、名称、空轨、坏 header 容错、错误身份、下载中销毁和保留方法。
|
||||
真实 1.2.0 插件配发布/候选核心,在 Chromium/Firefox/Windows WebKit 共 18 项通过:
|
||||
SRT 原生字幕显示与切换、挂起 fetch 销毁后继续安装、Blob URL 残留与 late reset。
|
||||
这是历史缺陷的复现成功,不代表已修复;LIFE/MERGE 保持 open,新增 HTTP/HOST 风险。
|
||||
完整 CI 2285 项通过(1937 单元、14 工程、334 基线),严格生产 TS 仍 346 文件;
|
||||
新增文件显式 lint 通过。声明指纹差异已核对仅为 CRLF,生产源码/声明/产物均未改。
|
||||
见[复现记录](changes/2026-09-13-PKG-MULTI-SUB-02-failures.md)和
|
||||
[验证证据](baselines/multiple-subtitles-failures.json)。实体设备、ASS 原生显示及完整组合仍待 05/06。
|
||||
222 项:119 done、13 doing、90 todo;196 项风险。独立本地任务提交,不推送、不发布。
|
||||
下一步 MULTI-SUB-03 拆分获取/解析合并/资源所有权并修复已复现缺陷;VTT 类型取舍继续待决。
|
||||
|
||||
|
||||
## PKG-MULTI-SUB-01 历史契约与 parser 来源完成
|
||||
|
||||
冻结三个实际 npm 版本、20 成员与 9 份 Git 输入;53 项本包契约/vendor 测试通过,
|
||||
|
||||
@@ -196,5 +196,7 @@
|
||||
| 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 |
|
||||
| MULTI-SUB-TYPE-01 | open / 已复现 | Declarations omit async registration, tracks/reset and make the subtitles array required | PKG-MULTI-SUB-04 |
|
||||
| MULTI-SUB-MERGE-01 | open / 已复现 | 1.0.0 merges cues by index while later versions concatenate independently timed track cues | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-LIFE-01 | open / 源码/产物事实 | Pending downloads and final object URL have no destroy owner | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-LIFE-01 | open / 已复现 | Pending downloads and final object URL have no destroy owner | PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-EXPORT-01 | open / 已复现 | Older CommonJS default objects conflict with export-assignment declarations; latest exports a direct factory | PKG-MULTI-SUB-04, PKG-MULTI-SUB-06 |
|
||||
| MULTI-SUB-HTTP-01 | open / 已复现 | HTTP error responses with parseable bodies are installed as subtitles | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
| MULTI-SUB-HOST-01 | open / 已复现 | Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources | PKG-MULTI-SUB-03, PKG-MULTI-SUB-05 |
|
||||
+47
-3
@@ -4275,7 +4275,11 @@
|
||||
"refactor/baselines/multiple-subtitles-release.json",
|
||||
"refactor/baselines/multiple-subtitles-contract.md",
|
||||
"test/multiple-subtitles.test.js",
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs",
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
],
|
||||
"compatibleResolution": "Keep historical merge profiles explicit and test real overlap/timing before choosing compatibility behavior.",
|
||||
"closureCriteria": "Unaligned lengths, overlap, sorting, names and real subtitle display across supported cores are verified."
|
||||
@@ -4283,7 +4287,7 @@
|
||||
{
|
||||
"id": "MULTI-SUB-LIFE-01",
|
||||
"title": "Pending downloads and final object URL have no destroy owner",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-02",
|
||||
@@ -4294,7 +4298,11 @@
|
||||
"refactor/baselines/multiple-subtitles-release.json",
|
||||
"refactor/baselines/multiple-subtitles-contract.md",
|
||||
"test/multiple-subtitles.test.js",
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs"
|
||||
"refactor/scripts/multiple-subtitles-contract.test.mjs",
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
],
|
||||
"compatibleResolution": "Reproduce stale completion and own requests and URLs without changing public selection methods.",
|
||||
"closureCriteria": "Cancellation, failures, source changes, repeated selection and destroy are verified with native core coverage."
|
||||
@@ -4316,6 +4324,42 @@
|
||||
],
|
||||
"compatibleResolution": "Preserve valid historical runtime import forms and explicitly assess type-only extraction conflicts.",
|
||||
"closureCriteria": "Actual installed CJS/ESM/global/legacy consumers and their declarations are verified."
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-HTTP-01",
|
||||
"title": "HTTP error responses with parseable bodies are installed as subtitles",
|
||||
"compatibleResolution": "Reject actual HTTP failures with explicit diagnostics while preserving successful conversion and original transport errors.",
|
||||
"closureCriteria": "Native HTTP errors, transport/body failures, sibling cancellation and successful requests have candidate regressions.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-03",
|
||||
"PKG-MULTI-SUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "MULTI-SUB-HOST-01",
|
||||
"title": "Subtitle init rejections are ignored and failed installation or URL replacement leaks or invalidates resources",
|
||||
"compatibleResolution": "Own installation results and URL rollback while preserving void selection methods and normal registration timing.",
|
||||
"closureCriteria": "Synchronous and asynchronous host failures, stale rejection, allocation failure and destroy leave no plugin-owned live resources.",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MULTI-SUB-03",
|
||||
"PKG-MULTI-SUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"test/multiple-subtitles-failures.test.js",
|
||||
"test/browser/multiple-subtitles-history.spec.js",
|
||||
"refactor/baselines/multiple-subtitles-failures.json",
|
||||
"refactor/changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-2
@@ -2277,11 +2277,14 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "VTT/SRT/ASS、编码/重叠 cue/排序/空轨/失败与切换",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"baselines/multiple-subtitles-failures.json",
|
||||
"changes/2026-09-13-PKG-MULTI-SUB-02-failures.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MULTI-SUB-03",
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { multipleSubtitlesHistorical } from '../helpers/multiple-subtitles.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main')
|
||||
const srt = text => `1\n00:00:00,000 --> 00:00:08,000\n${text}\n`
|
||||
const vtt = 'WEBVTT\n\n00:00.000 --> 00:08.000\nCached caption\n'
|
||||
|
||||
async function prepare(page, core) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(() => {
|
||||
window.createPlayer('/test/pattern.mp4')
|
||||
window.multipleProbe = { initialized: [], revoked: [], errors: [], signal: false, settled: false }
|
||||
window.multipleNativeFetch = window.fetch.bind(window)
|
||||
window.fetch = (url, options) => {
|
||||
if (url === '/test/multiple-held.vtt')
|
||||
window.multipleProbe.signal = Boolean(options?.signal)
|
||||
return window.multipleNativeFetch(url, options)
|
||||
}
|
||||
const revoke = URL.revokeObjectURL.bind(URL)
|
||||
URL.revokeObjectURL = (url) => {
|
||||
window.multipleProbe.revoked.push(url)
|
||||
revoke(url)
|
||||
}
|
||||
const init = window.art.subtitle.init.bind(window.art.subtitle)
|
||||
window.art.subtitle.init = (option) => {
|
||||
window.multipleProbe.initialized.push({ url: option.url, destroyed: window.art.isDestroy })
|
||||
const pending = init(option)
|
||||
// Observe the real host rejection without replacing its behavior or return value.
|
||||
Promise.resolve(pending).catch(error => window.multipleProbe.errors.push({ name: error.name, message: error.message }))
|
||||
return pending
|
||||
}
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
}
|
||||
|
||||
async function evidence(page, testInfo, core, observation) {
|
||||
const probe = await page.evaluate(() => window.multipleProbe)
|
||||
expect(probe.errors).toEqual([])
|
||||
await testInfo.attach('multiple-subtitles-history', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code) }, observation, probe }), contentType: 'application/json' })
|
||||
}
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`Multiple subtitles historical ${core} core: real SRT captions select, clear and reset`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-en.srt', route => route.fulfill({ body: srt('English'), contentType: 'text/plain' }))
|
||||
await page.route('**/test/multiple-jp.srt', route => route.fulfill({ body: srt('日本語'), contentType: 'text/plain' }))
|
||||
await prepare(page, core)
|
||||
await page.evaluate(async () => {
|
||||
window.multipleResult = await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'en', url: '/test/multiple-en.srt' }, { name: 'jp', url: '/test/multiple-jp.srt' }] }))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['jp']))
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveCount(0)
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
||||
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await evidence(page, testInfo, core, 'Native SRT conversion, track cue loading, real playback and DOM text selection/reset passed; this does not verify all CSS/device combinations.')
|
||||
})
|
||||
|
||||
test(`Multiple subtitles historical ${core} core: pending native fetch survives destroy and creates a retained Blob URL`, async ({ page }, testInfo) => {
|
||||
let release
|
||||
let requested
|
||||
const started = new Promise((resolve) => {
|
||||
requested = resolve
|
||||
})
|
||||
const pending = new Promise((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
await page.route('**/test/multiple-held.vtt', async (route) => {
|
||||
requested()
|
||||
await pending
|
||||
await route.fulfill({ body: vtt, contentType: 'text/vtt' })
|
||||
})
|
||||
try {
|
||||
await prepare(page, core)
|
||||
await page.evaluate(() => {
|
||||
window.multiplePending = window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-held.vtt', name: 'held' }] })(window.art).then((result) => {
|
||||
window.multipleResult = result
|
||||
window.multipleProbe.settled = true
|
||||
})
|
||||
})
|
||||
await started
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(await page.evaluate(() => ({ settled: window.multipleProbe.settled, signal: window.multipleProbe.signal, initialized: window.multipleProbe.initialized.length }))).toEqual({ settled: false, signal: false, initialized: 0 })
|
||||
release()
|
||||
await page.evaluate(() => window.multiplePending)
|
||||
const state = await page.evaluate(async () => {
|
||||
const [late] = window.multipleProbe.initialized
|
||||
const text = await (await window.multipleNativeFetch(late.url)).text()
|
||||
return { destroyedAtInit: late.destroyed, retained: !window.multipleProbe.revoked.includes(late.url), text }
|
||||
})
|
||||
expect(state.destroyedAtInit).toBe(true)
|
||||
expect(state.retained).toBe(true)
|
||||
expect(state.text).toContain('Cached caption')
|
||||
await evidence(page, testInfo, core, state)
|
||||
}
|
||||
finally { release() }
|
||||
})
|
||||
|
||||
test(`Multiple subtitles historical ${core} core: destroy retains the plugin URL and reset allocates another one`, async ({ page }, testInfo) => {
|
||||
await page.route('**/test/multiple-ready.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' }))
|
||||
await prepare(page, core)
|
||||
await page.evaluate(async () => {
|
||||
window.multipleResult = await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-ready.vtt', name: 'ready' }] })(window.art)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
const state = await page.evaluate(async () => {
|
||||
const first = window.multipleProbe.initialized[0].url
|
||||
window.art.destroy(false)
|
||||
const retainedText = await (await window.multipleNativeFetch(first)).text()
|
||||
const beforeReset = !window.multipleProbe.revoked.includes(first)
|
||||
const result = window.multipleResult.reset()
|
||||
const late = window.multipleProbe.initialized.at(-1)
|
||||
const lateText = await (await window.multipleNativeFetch(late.url)).text()
|
||||
return { beforeReset, retainedText, returnsUndefined: result === undefined, lateAfterDestroy: late.destroyed, newURL: late.url !== first, priorRevokedByReset: window.multipleProbe.revoked.includes(first), lateText }
|
||||
})
|
||||
expect(state.beforeReset).toBe(true)
|
||||
expect(state.retainedText).toContain('Cached caption')
|
||||
expect(state.returnsUndefined).toBe(true)
|
||||
expect(state.lateAfterDestroy).toBe(true)
|
||||
expect(state.newURL).toBe(true)
|
||||
expect(state.priorRevokedByReset).toBe(true)
|
||||
expect(state.lateText).toContain('Cached caption')
|
||||
await evidence(page, testInfo, core, state)
|
||||
})
|
||||
}
|
||||
@@ -28,12 +28,16 @@ export async function multipleSubtitlesHistorical() {
|
||||
return implementations
|
||||
}
|
||||
|
||||
export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {} } = {}) {
|
||||
export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {}, fetchResponse, coreUtils, onInit, onCreate } = {}) {
|
||||
const requests = []
|
||||
const requestOptions = []
|
||||
const blobs = new Map()
|
||||
const liveBlobs = new Map()
|
||||
const revoked = []
|
||||
const initialized = []
|
||||
const initStates = []
|
||||
const converted = []
|
||||
const listeners = new Map()
|
||||
const module = { exports: {} }
|
||||
const utils = {
|
||||
getExt: url => url.split('.').pop(),
|
||||
@@ -46,11 +50,32 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
converted.push(['ass', text])
|
||||
return subtitleVtt('ASS')
|
||||
},
|
||||
...coreUtils,
|
||||
}
|
||||
const art = {
|
||||
isDestroy: false,
|
||||
constructor: { utils },
|
||||
option: { subtitle: { escape: true, style: { color: 'red' } } },
|
||||
subtitle: { init(option) { initialized.push(option) } },
|
||||
subtitle: { init(option) {
|
||||
initialized.push(option)
|
||||
initStates.push({ destroyed: art.isDestroy })
|
||||
return onInit?.(option)
|
||||
} },
|
||||
on(name, callback) {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
return art
|
||||
},
|
||||
off(name, callback) {
|
||||
listeners.get(name)?.delete(callback)
|
||||
return art
|
||||
},
|
||||
}
|
||||
function responseFor(url) {
|
||||
const value = responses[url] ?? subtitleVtt(url)
|
||||
const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value
|
||||
return { ok: true, status: 200, arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }
|
||||
}
|
||||
const context = {
|
||||
Blob,
|
||||
@@ -58,16 +83,21 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
console: { log() {}, warn() {}, error() {} },
|
||||
URL: {
|
||||
createObjectURL(blob) {
|
||||
onCreate?.(blob)
|
||||
const url = `blob:subtitle-${blobs.size + 1}`
|
||||
blobs.set(url, blob)
|
||||
liveBlobs.set(url, blob)
|
||||
return url
|
||||
},
|
||||
revokeObjectURL(url) { revoked.push(url) },
|
||||
revokeObjectURL(url) {
|
||||
revoked.push(url)
|
||||
liveBlobs.delete(url)
|
||||
},
|
||||
},
|
||||
fetch: async (url) => {
|
||||
fetch: async (url, options) => {
|
||||
requests.push(url)
|
||||
const bytes = new TextEncoder().encode(responses[url] ?? subtitleVtt(url))
|
||||
return { ok: true, arrayBuffer: async () => bytes.buffer }
|
||||
requestOptions.push(options)
|
||||
return fetchResponse ? fetchResponse(url, options, responseFor) : responseFor(url)
|
||||
},
|
||||
}
|
||||
context.window = context
|
||||
@@ -79,5 +109,11 @@ export function multipleSubtitlesEnvironment(implementation, { script = false, r
|
||||
const exported = script ? context.artplayerPluginMultipleSubtitles : module.exports
|
||||
const factory = exported?.default || exported
|
||||
const latestText = async () => blobs.get(initialized.at(-1).url).text()
|
||||
return { factory, exported, art, utils, requests, blobs, revoked, initialized, converted, latestText }
|
||||
function emit(name, ...args) {
|
||||
if (name === 'destroy')
|
||||
art.isDestroy = true
|
||||
for (const callback of [...(listeners.get(name) || [])])
|
||||
callback(...args)
|
||||
}
|
||||
return { factory, exported, art, utils, requests, requestOptions, blobs, liveBlobs, revoked, initialized, initStates, converted, listeners, emit, responseFor, latestText }
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Historical failures use the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
import { multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const implementations = await multipleSubtitlesHistorical()
|
||||
const coreUtils = await loadModules({
|
||||
getExt: { file: 'packages/artplayer/src/utils/file', name: 'getExt' },
|
||||
srtToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'srtToVtt' },
|
||||
assToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'assToVtt' },
|
||||
})
|
||||
const subtitle = (url = 'a.vtt', name = 'a') => ({ url, name })
|
||||
const options = (...subtitles) => ({ subtitles })
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
async function flush() {
|
||||
for (let step = 0; step < 6; step++)
|
||||
await Promise.resolve()
|
||||
}
|
||||
const cues = rows => `WEBVTT\n\n${rows.map(([start, end, text]) => `00:0${start}.000 --> 00:0${end}.000\n${text}\n`).join('\n')}`
|
||||
const starts = text => [...text.matchAll(/^(\d{2}:\d{2}\.\d{3}) -->/gm)].map(match => match[1])
|
||||
|
||||
for (const implementation of implementations) {
|
||||
const create = setup => multipleSubtitlesEnvironment(implementation, setup)
|
||||
const oldMerge = implementation.version === '1.0.0'
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: actual core converters accept SRT, ASS and encoded VTT`, async () => {
|
||||
const responses = {
|
||||
'srt.SRT?track=1': '1\n00:00:00,000 --> 00:00:02,000\nSRT text\n',
|
||||
'a.ass': 'Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,,ASS text',
|
||||
'latin.vtt': Buffer.from(subtitleVtt('café'), 'latin1'),
|
||||
'utf16.vtt': Buffer.from(subtitleVtt('日本語'), 'utf16le'),
|
||||
}
|
||||
const env = create({ responses, coreUtils })
|
||||
await env.factory(options(subtitle('srt.SRT?track=1', 'srt'), subtitle('a.ass', 'ass'), { ...subtitle('latin.vtt', 'latin'), encoding: 'windows-1252' }, { ...subtitle('utf16.vtt', 'utf16'), encoding: 'utf-16le' }))(env.art)
|
||||
const text = env.utils.unescape(await env.latestText())
|
||||
for (const expected of ['SRT text', 'ASS text', 'café', '日本語'])
|
||||
assert(text.includes(expected))
|
||||
assert.equal(starts(text).length, oldMerge ? 1 : 4)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: response order does not reorder tracks`, async () => {
|
||||
const first = deferred()
|
||||
const second = deferred()
|
||||
const env = create({ fetchResponse: url => url === 'a.vtt' ? first.promise : second.promise })
|
||||
const pending = env.factory(options(subtitle(), subtitle('b.vtt', 'b')))(env.art)
|
||||
second.resolve(env.responseFor('b.vtt'))
|
||||
await flush()
|
||||
assert.equal(env.initialized.length, 0)
|
||||
first.resolve(env.responseFor('a.vtt'))
|
||||
await pending
|
||||
const text = env.utils.unescape(await env.latestText())
|
||||
assert(text.indexOf('art-subtitle-a') < text.indexOf('art-subtitle-b'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: fetch rejection preserves error while sibling work continues`, async () => {
|
||||
const error = new Error('fetch failed')
|
||||
const sibling = deferred()
|
||||
let bodyRead = false
|
||||
const env = create({ fetchResponse: url => url === 'a.vtt' ? Promise.reject(error) : sibling.promise })
|
||||
await assert.rejects(env.factory(options(subtitle(), subtitle('b.vtt', 'b')))(env.art), caught => caught === error)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
assert.deepEqual(env.requestOptions, [undefined, undefined])
|
||||
sibling.resolve({ arrayBuffer: async () => {
|
||||
bodyRead = true
|
||||
return new TextEncoder().encode(subtitleVtt('late')).buffer
|
||||
} })
|
||||
await flush()
|
||||
assert.equal(bodyRead, true)
|
||||
assert.equal(env.blobs.size, 0)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: HTTP errors with parseable bodies still mount`, async () => {
|
||||
const env = create({ fetchResponse: (_url, _options, responseFor) => ({ ...responseFor('a.vtt'), ok: false, status: 404 }) })
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert.equal(env.initialized.length, 1)
|
||||
assert((await env.latestText()).includes('a.vtt'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: body and decoder errors reject before allocating URLs`, async () => {
|
||||
const error = new Error('body failed')
|
||||
const env = create({ fetchResponse: () => ({ arrayBuffer: async () => {
|
||||
throw error
|
||||
} }) })
|
||||
await assert.rejects(env.factory(options(subtitle()))(env.art), caught => caught === error)
|
||||
assert.equal(env.blobs.size, 0)
|
||||
const invalid = create()
|
||||
await assert.rejects(invalid.factory(options({ ...subtitle(), encoding: 'invalid-encoding-label' }))(invalid.art), { name: 'RangeError' })
|
||||
assert.equal(invalid.blobs.size, 0)
|
||||
assert.equal(invalid.initialized.length, 0)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: missing names throw before replacing the active URL`, async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options(subtitle()))(env.art)
|
||||
assert.throws(() => result.tracks(['missing']), { name: 'TypeError' })
|
||||
assert.deepEqual(env.revoked, [''])
|
||||
assert.equal(env.liveBlobs.has('blob:subtitle-1'), true)
|
||||
result.reset()
|
||||
assert.equal(env.initialized.length, 2)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: duplicate names select the first parsed track`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': subtitleVtt('First text'), 'b.vtt': subtitleVtt('Second text') } })
|
||||
const result = await env.factory(options(subtitle('a.vtt', 'same'), subtitle('b.vtt', 'same')))(env.art)
|
||||
result.tracks(['same'])
|
||||
const text = await env.latestText()
|
||||
assert(text.includes('First text'))
|
||||
assert(!text.includes('Second text'))
|
||||
result.reset()
|
||||
assert((await env.latestText()).includes('Second text'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: a shorter later track exposes the old index merge failure`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': cues([[0, 2, 'A'], [3, 5, 'B']]), 'b.vtt': subtitleVtt('C') } })
|
||||
const pending = env.factory(options(subtitle(), subtitle('b.vtt', 'b')))(env.art)
|
||||
if (oldMerge) {
|
||||
await assert.rejects(pending, { name: 'TypeError' })
|
||||
assert.equal(env.blobs.size, 0)
|
||||
}
|
||||
else {
|
||||
await pending
|
||||
assert.equal(starts(await env.latestText()).length, 3)
|
||||
}
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: a longer later track used to lose its tail`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': subtitleVtt('A'), 'b.vtt': cues([[0, 2, 'B'], [3, 5, 'Tail']]) } })
|
||||
await env.factory(options(subtitle(), subtitle('b.vtt', 'b')))(env.art)
|
||||
const text = await env.latestText()
|
||||
assert.equal(text.includes('Tail'), !oldMerge)
|
||||
assert.equal(starts(text).length, oldMerge ? 1 : 3)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: each track sorts internally without globally sorting the merge`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': cues([[2, 6, 'A2'], [0, 4, 'A0']]), 'b.vtt': cues([[5, 7, 'B5'], [1, 3, 'B1']]) } })
|
||||
await env.factory(options(subtitle(), subtitle('b.vtt', 'b')))(env.art)
|
||||
assert.deepEqual(starts(await env.latestText()), oldMerge
|
||||
? ['00:00.000', '00:02.000']
|
||||
: ['00:00.000', '00:02.000', '00:01.000', '00:05.000'])
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: parser diagnostics are discarded but usable cues survive`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': subtitleVtt('usable').replace('WEBVTT', 'BAD HEADER') } })
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert((await env.latestText()).includes('usable'))
|
||||
const empty = create({ responses: { 'a.vtt': 'WEBVTT\n\n', 'unknown.bin': 'not subtitles' } })
|
||||
await empty.factory(options(subtitle(), subtitle('unknown.bin', 'unknown')))(empty.art)
|
||||
assert.equal(await empty.latestText(), 'WEBVTT\n\n')
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: destroy while fetching still permits late registration and leaks its URL`, async () => {
|
||||
const request = deferred()
|
||||
const env = create({ fetchResponse: () => request.promise })
|
||||
let settled = false
|
||||
const pending = env.factory(options(subtitle()))(env.art).then((result) => {
|
||||
settled = true
|
||||
return result
|
||||
})
|
||||
env.emit('destroy')
|
||||
await flush()
|
||||
assert.equal(settled, false)
|
||||
assert.equal(env.requestOptions[0], undefined)
|
||||
request.resolve(env.responseFor('a.vtt'))
|
||||
assert.equal((await pending).name, 'multipleSubtitles')
|
||||
assert.deepEqual(env.initStates, [{ destroyed: true }])
|
||||
assert.equal(env.liveBlobs.size, 1)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: destroy during body decoding also permits late installation`, async () => {
|
||||
const body = deferred()
|
||||
const started = deferred()
|
||||
const env = create({ fetchResponse: () => ({ arrayBuffer: () => {
|
||||
started.resolve()
|
||||
return body.promise
|
||||
} }) })
|
||||
const pending = env.factory(options(subtitle()))(env.art)
|
||||
await started.promise
|
||||
env.emit('destroy')
|
||||
body.resolve(new TextEncoder().encode(subtitleVtt('late body')).buffer)
|
||||
await pending
|
||||
assert.deepEqual(env.initStates, [{ destroyed: true }])
|
||||
assert.equal(env.liveBlobs.size, 1)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: retained methods allocate new URLs after destroy`, async () => {
|
||||
const env = create()
|
||||
const result = await env.factory(options(subtitle()))(env.art)
|
||||
env.emit('destroy')
|
||||
assert.equal(env.liveBlobs.has('blob:subtitle-1'), true)
|
||||
assert.equal(result.tracks([]), undefined)
|
||||
result.reset()
|
||||
assert.deepEqual(env.initStates, [{ destroyed: false }, { destroyed: true }, { destroyed: true }])
|
||||
assert.equal(env.liveBlobs.has('blob:subtitle-3'), true)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: synchronous subtitle init failure leaves its new URL owned by nobody`, async () => {
|
||||
const error = new Error('init failed')
|
||||
const env = create({ onInit: () => {
|
||||
throw error
|
||||
} })
|
||||
await assert.rejects(env.factory(options(subtitle()))(env.art), caught => caught === error)
|
||||
assert.equal(env.liveBlobs.size, 1)
|
||||
assert.equal(env.art.option.subtitle.escape, false)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: registration ignores an observed asynchronous init rejection`, async () => {
|
||||
const error = new Error('async init failed')
|
||||
const errors = []
|
||||
const env = create({ onInit: () => {
|
||||
const rejected = Promise.reject(error)
|
||||
rejected.catch(caught => errors.push(caught))
|
||||
return rejected
|
||||
} })
|
||||
assert.equal((await env.factory(options(subtitle()))(env.art)).name, 'multipleSubtitles')
|
||||
await flush()
|
||||
assert.deepEqual(errors, [error])
|
||||
assert.equal(env.liveBlobs.size, 1)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: URL allocation failure happens after revoking the prior active URL`, async () => {
|
||||
const error = new Error('allocation failed')
|
||||
let calls = 0
|
||||
const env = create({ onCreate: () => {
|
||||
if (++calls === 2)
|
||||
throw error
|
||||
} })
|
||||
const result = await env.factory(options(subtitle()))(env.art)
|
||||
assert.throws(() => result.reset(), caught => caught === error)
|
||||
assert.equal(env.liveBlobs.size, 0)
|
||||
assert.equal(env.initialized.length, 1)
|
||||
assert.deepEqual(env.revoked, ['', 'blob:subtitle-1'])
|
||||
result.reset()
|
||||
assert.equal(env.liveBlobs.size, 1)
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: metadata remains live after download and restart does not reload`, async () => {
|
||||
const request = deferred()
|
||||
const env = create({ fetchResponse: () => request.promise })
|
||||
const entry = subtitle()
|
||||
const pending = env.factory(options(entry))(env.art)
|
||||
entry.name = 'renamed'
|
||||
request.resolve(env.responseFor('a.vtt'))
|
||||
const result = await pending
|
||||
env.art.option.url = 'next.mp4'
|
||||
env.emit('restart')
|
||||
result.tracks(['renamed'])
|
||||
assert(env.utils.unescape(await env.latestText()).includes('art-subtitle-renamed'))
|
||||
assert.deepEqual(env.requests, ['a.vtt'])
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user