test(multiple-subtitles): [PKG-MULTI-SUB-02] reproduce failures and native resource leaks

This commit is contained in:
Harvey Zhao committed 2026-09-13 10:38:32 +08:00
1 parent 5a0d94cb89
commit 36cd151095
12 files changed
+5399 -19

No files matched your search

+2 -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/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
View File
@@ -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)
+16
View File
@@ -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 测试通过,
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
})
}
+43 -7
View File
@@ -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 }
}
+260
View File
@@ -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'])
})
}