test(chromecast): [PKG-CAST-02] reproduce SDK and lifecycle failures

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:42:46 +08:00
1 parent 30de007102
commit c723c70251
9 files changed
+450 -6

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 test/asr-distribution.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js 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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js",
"test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js 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/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.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",
@@ -97,7 +97,7 @@
"test:asr": "node --test test/asr.test.js test/asr-audio.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-distribution.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js",
"test:asr-types-package": "node refactor/scripts/asr-package-types.mjs",
"test:asr-demo": "node refactor/scripts/asr-local-demo.mjs",
"test:chromecast": "node --test test/chromecast.test.js"
"test:chromecast": "node --test test/chromecast.test.js test/chromecast-failures.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -0,0 +1,63 @@
{
"schemaVersion": 1,
"task": "PKG-CAST-02",
"capturedAt": "2026-09-13T08:39:20.323Z",
"sourceCommit": "146b78d5767bba88612184869155a39317794537",
"tests": {
"passed": 93,
"public": 28,
"additional": 65,
"failed": 0,
"skipped": 0,
"cancelled": 0,
"file": "refactor/.cache/chromecast02-tests.log",
"sha256": "47c5203986e2dbabd372fe27349d986bde52e77be1cae4462b7c827d73b2a466"
},
"inputs": {
"test/helpers/chromecast.js": "a51ad278dffde582c00d66cd3c0ea868141548d349a80a24a355856378ab5de7",
"test/chromecast.test.js": "be9d42e13ea495d7c266732ffe0273acb96d1a29b8ef330eba32156aaa5eccb8",
"test/chromecast-failures.test.js": "53660df362e5d55d9bd87ff1a038073ce39982e3307f79ee2c41f91bd8fe4bd2",
"refactor/baselines/chromecast-release.json": "a68745786d976afee10afb2bb0fd61aeba8f9fffa18737af3e395d25079e3eec"
},
"observations": [
{
"id": "CAST-SDK-01",
"title": "Cast requestSession completion is incorrectly passed to loadMedia as a session",
"confirmed": "Controlled SDK and real frozen plugin code; not a physical Cast session."
},
{
"id": "CAST-LOADER-01",
"title": "Cast readiness waits hang or overwrite each other across ready SDKs, repeated clicks and multiple instances",
"confirmed": "Controlled SDK and real frozen plugin code; not a physical Cast session."
},
{
"id": "CAST-LIFE-01",
"title": "Cast listeners and continuations survive player destruction and update another player icon",
"confirmed": "Controlled SDK and real frozen plugin code; not a physical Cast session."
},
{
"id": "CAST-STATE-01",
"title": "Cast terminal session states leave the normalized callback and icon connected",
"confirmed": "Controlled SDK and real frozen plugin code; not a physical Cast session."
},
{
"id": "CAST-ERROR-01",
"title": "Cast media loading rejection escapes on a detached Promise while the click has already fulfilled",
"confirmed": "Controlled SDK and real frozen plugin code; not a physical Cast session."
}
],
"preserved": [
"Latest getter returns raw SessionState, initially null.",
"Callbacks receive their original option as this and are looked up live.",
"Current art.option.url is used when option.url is absent.",
"1.0 eager registration and name-only result are tested separately."
],
"sourceReferences": [
"https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastContext",
"https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastSession#loadMedia"
],
"sourceChanged": false,
"deviceValidated": false,
"sdkNetworkLoaded": false,
"releaseReady": false
}
@@ -0,0 +1,31 @@
# PKG-CAST-02 受控 SDK 与错误基线
新增65项1.1.0及冻结工作区缺陷/边界观察,连同01的28项公共行为共93项通过。
这些测试验证历史错误确实可复现,不代表候选已修复;所有SDK/DOM均明确受控,
没有连接真实Cast设备、请求远程SDK或验证实际媒体播放。
## 已复现与下一步修正
| 风险 | 精确复现 | 03的修复方向 |
| --- | --- | --- |
| CAST-SDK-01 | requestSession成功返回undefined,旧click把它当session后在loadMedia处TypeError | 请求结算后读取getCurrentSession并处理null和过期归属 |
| CAST-LOADER-01 | 已就绪Framework仍等待过去的availability;两实例覆盖handler、插两脚本、首click不结算;回调中setOptions异常逃逸 | 共享可重用加载协调、真实ready检测、保留外部handler、单一错误边界 |
| CAST-LIFE-01 | 无destroy订阅/解绑,销毁后仍回调和加载;第二实例改第一图标;复用工厂共享状态 | 实例范围控制器、控件节点归属、代际取消及独立监听释放,不擅自结束共享会话 |
| CAST-STATE-01 | SESSION_ENDED更新原始getter但图标/归一化回调仍connected | 完整终态映射,保持getter返回SessionState |
| CAST-ERROR-01 | loadMedia拒绝另起脱离链,click已fulfilled而末端Promise拒绝 | 返回并await完整加载链,原错误与单次onError、notice有明确归属 |
另验证false availability、用户拒绝会话、脚本失败后重试残留、当前art.option.url、
live回调替换与this===option。SDK自身返回的Promise链由测试独立观察并附拒绝处理,
没有修改插件全局Promise或把孤立拒绝伪装成click拒绝。
来源规范:[CastContext](https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastContext)、
[CastSession.loadMedia](https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastSession#loadMedia)、
[SessionState](https://developers.google.com/cast/docs/reference/web_sender/cast.framework#SessionState)。
1.0.0的提前SDK加载、仅name结果、已有SDK可立即注册及原始加载拒绝在01中单独冻结;
不把1.1.0新增的回调/getter测试强行套给1.0.0,也不将它列为skip。
新增失败文件接入test:chromecast及test:unit。精确输入和日志哈希见
[证据](../baselines/chromecast-failures-validation.json)。生产源码/声明/产物未改,不进行
无意义重构建。03接着拆分loader/SDK适配/实例会话/UI与清理,并解决VENDOR-10图标
来源问题;04处理精确类型,05保留真实HTTPS Chrome/接收设备/媒体可达性门槛。
这些问题按已有任务修复,不仅记录后搁置;本步单独提交,不推送或发布。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。
状态:todo 80 / doing 17 / blocked 0 / done 131 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 79 / doing 17 / blocked 0 / done 132 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -288,7 +288,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-CAST-01 | artplayer-plugin-chromecast<br>核对包契约与历史用法 | BASE-05 | url/sdk/icon/mimeType、loader、会话、全局 SDK 与结果 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-CAST-02 | artplayer-plugin-chromecast<br>建立特有行为与错误测试 | PKG-CAST-01, ENG-03, ENG-05 | SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-CAST-02 | artplayer-plugin-chromecast<br>建立特有行为与错误测试 | PKG-CAST-01, ENG-03, ENG-05 | SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-CAST-03 | artplayer-plugin-chromecast<br>整理内部职责与资源 | PKG-CAST-02, CORE-11, CORE-13 | SDK singleton/实例订阅/会话与控件职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-CAST-04 | artplayer-plugin-chromecast<br>迁移自有源码和公开类型 | PKG-CAST-03, ENG-04, ENG-06, CORE-07 | Cast 能力 adapter、全局对象和元数据类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-CAST-05 | artplayer-plugin-chromecast<br>验证新旧核心和组合 | PKG-CAST-04, CORE-22 | stub 测试之外记录实际 Cast 设备会话、源更新和断开 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
@@ -543,6 +543,7 @@
- PKG-VAST-02: [记录](changes/2026-09-12-PKG-VAST-02-tests.md) [记录](baselines/vast-behavior-validation.json) [记录](baselines/vast-core.json) [记录](vast-validation.md)
- PKG-VAST-03: [记录](changes/2026-09-12-PKG-VAST-03-lifecycle.md) [记录](baselines/vast-lifecycle-validation.json) [记录](vast-compatibility-decision.md)
- PKG-CAST-01: [记录](baselines/chromecast-release.json) [记录](baselines/chromecast-contract.md) [记录](baselines/chromecast-contract-validation.json) [记录](changes/2026-09-13-PKG-CAST-01-contracts.md)
- PKG-CAST-02: [记录](changes/2026-09-13-PKG-CAST-02-failures.md) [记录](baselines/chromecast-failures-validation.json)
- PKG-DPIP-01: [记录](changes/2026-09-12-PKG-DPIP-01-contract.md) [记录](baselines/dpip-release.json) [记录](baselines/dpip-contract.md) [记录](baselines/dpip-contract-validation.json) [记录](dpip-validation.md)
- PKG-DPIP-02: [记录](changes/2026-09-12-PKG-DPIP-02-tests.md) [记录](baselines/dpip-behavior-validation.json) [记录](dpip-validation.md)
- PKG-DPIP-03: [记录](changes/2026-09-12-PKG-DPIP-03-lifecycle.md) [记录](baselines/dpip-lifecycle-validation.json) [记录](dpip-validation.md)
+11
View File
@@ -1,5 +1,16 @@
# 进度与证据
## PKG-CAST-02 Chromecast 错误基线完成
93项受控历史测试通过,其中65项新增错误/生命周期观察。首次会话结果误用、已就绪
SDK悬空/多实例覆盖、错图标与销毁后加载、终态遗漏、脱离媒体Promise均已复现并
登记03修复任务。live选项/回调this/原始SessionState继续保留。见[变更](changes/2026-09-13-PKG-CAST-02-failures.md)
与[证据](baselines/chromecast-failures-validation.json)。生产仍未改,真实Cast设备与
远程SDK尚未验证。228项:132 done、17 doing、79 todo;217风险。
下一步03生产模块与资源修复;已获授权的Canvas/Ambilight类型修复正由两个子代理
独立实施,父代理协调共享类型检查和编辑器生成。没有推送或发布。
## PKG-CAST-01 Chromecast 契约与来源完成
冻结真实npm1.0.0/1.1.0与9个工作区输入,逐成员、入口及git关联核验通过。1.0.0
+5
View File
@@ -216,3 +216,8 @@
| ASR-RESTART-01 | resolved / 已复现 | Delayed captured-source restart resumes recognition after a newer pause or stop | PKG-ASR-09 |
| VENDOR-10 | open / 源码/产物事实 | Chromecast embeds a Font Awesome Pro path with no established redistribution entitlement | PKG-CAST-03, PKG-CAST-06 |
| CAST-TYPE-01 | open / 源码/产物事实 | Chromecast declarations describe a synchronous name-only registration and omit existing runtime callbacks and query methods | PKG-CAST-04 |
| CAST-SDK-01 | open / 已复现 | Cast requestSession completion is incorrectly passed to loadMedia as a session | PKG-CAST-03, PKG-CAST-05 |
| CAST-LOADER-01 | open / 已复现 | Cast readiness waits hang or overwrite each other across ready SDKs, repeated clicks and multiple instances | PKG-CAST-03, PKG-CAST-05 |
| CAST-LIFE-01 | open / 已复现 | Cast listeners and continuations survive player destruction and update another player icon | PKG-CAST-03, PKG-CAST-05 |
| CAST-STATE-01 | open / 已复现 | Cast terminal session states leave the normalized callback and icon connected | PKG-CAST-03, PKG-CAST-05 |
| CAST-ERROR-01 | open / 已复现 | Cast media loading rejection escapes on a detached Promise while the click has already fulfilled | PKG-CAST-03, PKG-CAST-05 |
+80
View File
@@ -4745,6 +4745,86 @@
],
"compatibleResolution": "Preserve the latest published root factory type and provide a precise runtime type view; verify valid historical JavaScript module forms independently.",
"closureCriteria": "Frozen type consumers and replacements pass, accurate async/callback/state declarations work through an additive entry, and actual installed exports are tested."
},
{
"id": "CAST-SDK-01",
"title": "Cast requestSession completion is incorrectly passed to loadMedia as a session",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-05"
],
"evidence": [
"test/chromecast-failures.test.js",
"refactor/baselines/chromecast-failures-validation.json"
],
"compatibleResolution": "Retrieve the actual current session after request success and validate live instance ownership.",
"closureCriteria": "Successful undefined/null request result loads through the current session; absence and refusal preserve appropriate original errors."
},
{
"id": "CAST-LOADER-01",
"title": "Cast readiness waits hang or overwrite each other across ready SDKs, repeated clicks and multiple instances",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-05"
],
"evidence": [
"test/chromecast-failures.test.js",
"refactor/baselines/chromecast-failures-validation.json"
],
"compatibleResolution": "Coordinate one reusable loader per document with readiness detection, shared pending work and complete error settlement.",
"closureCriteria": "Ready, pending, failure, retry, external callback and multiple instances are covered without duplicate scripts or stranded callers."
},
{
"id": "CAST-LIFE-01",
"title": "Cast listeners and continuations survive player destruction and update another player icon",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-05"
],
"evidence": [
"test/chromecast-failures.test.js",
"refactor/baselines/chromecast-failures-validation.json"
],
"compatibleResolution": "Use per-registration lifecycle ownership, scoped control nodes and obsolete-operation checks without ending a page-shared session.",
"closureCriteria": "Destroyed players get no late media loads or callbacks; subscriptions are removed and separate players keep independent UI."
},
{
"id": "CAST-STATE-01",
"title": "Cast terminal session states leave the normalized callback and icon connected",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-05"
],
"evidence": [
"test/chromecast-failures.test.js",
"refactor/baselines/chromecast-failures-validation.json"
],
"compatibleResolution": "Map terminal states without changing the raw SessionState query API.",
"closureCriteria": "Ended and failed states update UI and callbacks while getCastState keeps original SessionState values."
},
{
"id": "CAST-ERROR-01",
"title": "Cast media loading rejection escapes on a detached Promise while the click has already fulfilled",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-CAST-03",
"PKG-CAST-05"
],
"evidence": [
"test/chromecast-failures.test.js",
"refactor/baselines/chromecast-failures-validation.json"
],
"compatibleResolution": "Await the complete media-loading chain and preserve one original error notification.",
"closureCriteria": "loadMedia failure reaches the click promise exactly once, no orphan rejection or duplicated error callback; success ordering remains documented."
}
]
}
+5 -2
View File
@@ -3015,11 +3015,14 @@
"ENG-03",
"ENG-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "SDK 脚本加载失败/重入、无设备/拒绝、重复实例与销毁",
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
"evidence": []
"evidence": [
"changes/2026-09-13-PKG-CAST-02-failures.md",
"baselines/chromecast-failures-validation.json"
]
},
{
"id": "PKG-CAST-03",
+250
View File
@@ -0,0 +1,250 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Historical Chromecast defects remain explicit observations.
import test from 'node:test'
import { chromecastEnvironment, chromecastHistorical } from './helpers/chromecast.js'
for (const implementation of await chromecastHistorical()) {
// Version 1.0 loads during registration and has no query/callback surface.
// Its distinct public behavior is frozen in chromecast.test.js.
if (implementation.name.startsWith('published-1.0.0-'))
continue
test(`Chromecast ${implementation.name}: available Framework still waits for an availability callback that already happened`, async () => {
const env = chromecastEnvironment(implementation)
env.installSdk()
const player = env.player()
await env.factory({})(player.art)
let settled = false
player.controls[0].click().then(() => {
settled = true
}, () => {
settled = true
})
await env.flush()
assert.equal(settled, false)
assert.equal(env.scripts.length, 0)
assert.equal(env.sdkOptions.length, 0)
assert.equal(typeof env.context.__onGCastApiAvailable, 'function')
})
test(`Chromecast ${implementation.name}: successful requestSession completion is incorrectly used as a session`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const errors = []
await env.factory({ onError: error => errors.push(error) })(player.art)
const pending = player.controls[0].click()
env.installSdk({ requestResult: undefined })
env.context.__onGCastApiAvailable(true)
await assert.rejects(pending, error => error.name === 'TypeError' && error.message.includes('loadMedia'))
assert.equal(env.requests(), 1)
assert.equal(env.mediaLoads.length, 0)
assert.equal(errors.length, 1)
assert.equal(player.art.notice.show, 'Error connecting to cast session')
})
test(`Chromecast ${implementation.name}: concurrent instances overwrite availability and strand the first click`, async () => {
const env = chromecastEnvironment(implementation)
const first = env.player()
const second = env.player()
await env.factory({})(first.art)
await env.factory({})(second.art)
let firstSettled = false
first.controls[0].click().then(() => {
firstSettled = true
}, () => {
firstSettled = true
})
const firstHandler = env.context.__onGCastApiAvailable
const next = second.controls[0].click()
assert.notEqual(env.context.__onGCastApiAvailable, firstHandler)
assert.equal(env.scripts.length, 2)
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await next
await env.flush()
assert.equal(firstSettled, false)
assert.equal(first.icon.style.color, 'red', 'Second instance modifies the first global icon')
assert.equal(second.icon.style.color, undefined)
})
test(`Chromecast ${implementation.name}: reusing one factory shares session state across players`, async () => {
const env = chromecastEnvironment(implementation)
const first = env.player()
const second = env.player()
const register = env.factory({})
const a = await register(first.art)
const b = await register(second.art)
const pending = second.controls[0].click()
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await pending
assert.equal(a.getCastState(), 'SESSION_STARTED')
assert.equal(a.isCasting(), true)
assert.equal(b.isCasting(), true)
assert.equal(first.icon.style.color, 'red')
assert.equal(second.icon.style.color, undefined)
})
test(`Chromecast ${implementation.name}: ended state changes the raw getter but leaves callback and icon connected`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const states = []
const plugin = await env.factory({ onStateChange: value => states.push(value) })(player.art)
const pending = player.controls[0].click()
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await pending
env.sessionState('SESSION_ENDED', null)
assert.equal(plugin.getCastState(), 'SESSION_ENDED')
assert.equal(plugin.isCasting(), false)
assert.deepEqual(states, ['connected'])
assert.equal(player.icon.style.color, 'red')
})
test(`Chromecast ${implementation.name}: media rejection is detached from the fulfilled click promise`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const error = new Error('receiver rejected media')
const errors = []
await env.factory({ onError: value => errors.push(value) })(player.art)
const pending = player.controls[0].click()
env.installSdk({ loadError: error })
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
assert.equal(await pending, undefined)
await env.flush()
await assert.rejects(env.pendingMedia.at(-1), value => value === error)
assert.deepEqual(errors, [error])
assert.equal(player.art.notice.show, 'Error casting media')
})
test(`Chromecast ${implementation.name}: destroy leaves SDK listeners and callbacks active`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const availability = []
await env.factory({ onCastAvailable: value => availability.push(value) })(player.art)
const pending = player.controls[0].click()
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await pending
player.destroy()
env.emit('caststatechanged', { castState: 'NO_DEVICES_AVAILABLE' })
assert.deepEqual(availability, [false])
assert.equal(player.listeners.size, 0)
assert.equal(env.sdkListeners.get('sessionstatechanged').length, 1)
assert.equal(env.sdkListeners.get('caststatechanged').length, 1)
})
test(`Chromecast ${implementation.name}: exception inside availability escapes without settling initialization`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
await env.factory({})(player.art)
let settled = false
player.controls[0].click().then(() => {
settled = true
}, () => {
settled = true
})
const error = new Error('CastOptions failed')
const { castContext } = env.installSdk()
castContext.setOptions = () => {
throw error
}
assert.throws(() => env.context.__onGCastApiAvailable(true), value => value === error)
await env.flush()
assert.equal(settled, false)
})
test(`Chromecast ${implementation.name}: unavailable API rejects with one original callback error`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const errors = []
const option = { onError(error) {
errors.push({ error, receiver: this })
} }
await env.factory(option)(player.art)
const pending = player.controls[0].click()
env.context.__onGCastApiAvailable(false)
await assert.rejects(pending, error => error.message === 'Cast API is not available' && error === errors[0].error)
assert.equal(errors.length, 1)
assert.equal(errors[0].receiver, option)
assert.equal(env.scripts.length, 1, 'The failed loader retains its script')
})
test(`Chromecast ${implementation.name}: session request rejection preserves original error and notice`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
const error = new Error('User declined the session')
const errors = []
await env.factory({ onError: value => errors.push(value) })(player.art)
const pending = player.controls[0].click()
env.installSdk({ requestError: error })
env.context.__onGCastApiAvailable(true)
await assert.rejects(pending, value => value === error)
assert.deepEqual(errors, [error])
assert.equal(env.requests(), 1)
assert.equal(env.mediaLoads.length, 0)
assert.equal(player.art.notice.show, 'Error connecting to cast session')
})
test(`Chromecast ${implementation.name}: current player source and live callback receiver are used on subsequent casting`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player('/first.mp4')
const calls = []
const option = { onCastStart() {
calls.push(this)
} }
await env.factory(option)(player.art)
const pending = player.controls[0].click()
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await pending
await env.flush()
player.art.option.url = '/next.webm'
option.onCastStart = function () {
calls.push(['replacement', this])
}
await player.controls[0].click()
await env.flush()
assert.equal(env.mediaLoads.at(-1).media.contentId, '/next.webm')
assert.equal(env.mediaLoads.at(-1).media.contentType, 'video/webm')
assert.equal(calls[0], option)
assert.equal(calls[1][0], 'replacement')
assert.equal(calls[1][1], option)
})
test(`Chromecast ${implementation.name}: failed script retry retains old script and replaces the global callback`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
await env.factory({})(player.art)
const first = player.controls[0].click()
const previous = env.context.__onGCastApiAvailable
const error = new Error('script failed')
env.scripts[0].onerror(error)
await assert.rejects(first, value => value === error)
const second = player.controls[0].click()
assert.equal(env.scripts.length, 2)
assert.notEqual(env.context.__onGCastApiAvailable, previous)
env.scripts[1].onerror(error)
await assert.rejects(second, value => value === error)
})
test(`Chromecast ${implementation.name}: late SDK availability after destroy still loads media`, async () => {
const env = chromecastEnvironment(implementation)
const player = env.player()
await env.factory({})(player.art)
const pending = player.controls[0].click()
player.destroy()
env.installSdk()
env.context.__onGCastApiAvailable(true)
env.sessionState('SESSION_STARTED')
await pending
await env.flush()
assert.equal(env.mediaLoads.length, 1)
assert.equal(player.art.notice.show, 'Casting started')
})
}