mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 22:06:17 -08:00
test(ambilight): [PKG-AMBILIGHT-02] reproduce historical frame and native canvas failures
This commit is contained in:
1 parent
06032a6906
commit
43f0e9a8c6
11 files changed
+435
-10
No files matched your search
+3
-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/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/ambilight.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",
|
||||
@@ -69,7 +69,8 @@
|
||||
"check:commits": "node refactor/scripts/commit-audit.mjs",
|
||||
"check:impact": "node refactor/scripts/impact.mjs",
|
||||
"check:contracts": "node refactor/scripts/contracts.mjs",
|
||||
"test:contracts": "node refactor/scripts/run-contracts.mjs"
|
||||
"test:contracts": "node refactor/scripts/run-contracts.mjs",
|
||||
"test:ambilight": "node --test test/ambilight.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -11,3 +11,16 @@
|
||||
复现问题;04兼容类型及旧CJS.default;05/06做真实媒体、代理、浏览器和安装包验证。
|
||||
stop保留最后画面,核心destroy终止资源。zIndex仍忽略,参数省略及两版声明差异分别
|
||||
处理;Git关联不构成连续核心支持范围。
|
||||
|
||||
## 02历史回归
|
||||
|
||||
`yarn test:ambilight`运行30项Node回归并接入test:unit;正常参数/频率/暂停与已确认的
|
||||
历史问题分别断言。`yarn test:browser test/browser/ambilight.spec.js`运行三实现/三核心/
|
||||
三引擎真实媒体;核心标签published是实际npm5.4.0,published-5.1.7是实际npm5.1.7,
|
||||
candidate是当前源码;以browser-evidence的manifest和实际version为准。
|
||||
跨origin使用同一服务的localhost与127.0.0.1,读像素抛原生SecurityError;换回同源
|
||||
后持续错误是待03修复的历史污染。Node零尺寸记录只证明未保护,未模拟为浏览器结论。
|
||||
|
||||
02最终30项Node、54项三引擎/三核心浏览器通过,未skip;新建Canvas能读取已恢复的
|
||||
同源视频,而旧插件Canvas仍抛SecurityError。主CI955项通过(另44项重复观察)。
|
||||
完整报告与输入来源见baselines/ambilight-behavior-validation.json。03开始修复这些问题。
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "PKG-AMBILIGHT-02",
|
||||
"status": "done",
|
||||
"sourceCommit": "06032a6906aa2a5c59e421cd8076d70daf5f4801",
|
||||
"checks": [
|
||||
{
|
||||
"command": "node --test test/ambilight.test.js",
|
||||
"result": "pass",
|
||||
"tests": 30,
|
||||
"scope": "Three frozen implementations; normal control/parameter semantics and historical failure assertions",
|
||||
"log": "refactor/.cache/ambilight02-tests.log"
|
||||
},
|
||||
{
|
||||
"command": "playwright test test/browser/ambilight.spec.js",
|
||||
"result": "pass",
|
||||
"tests": 54,
|
||||
"projects": [
|
||||
"chromium",
|
||||
"firefox",
|
||||
"webkit"
|
||||
],
|
||||
"coreSources": [
|
||||
"npm 5.1.7",
|
||||
"npm 5.4.0",
|
||||
"candidate source"
|
||||
],
|
||||
"skipped": 0,
|
||||
"scope": "Native video/Canvas/RAF, local source switch, retained DOM and actual cross-origin taint; fresh native canvas reads recovered video while old plugin canvas still fails",
|
||||
"report": "refactor/.cache/ambilight02-final-results/report.json",
|
||||
"archivedResults": "refactor/.cache/ambilight02-final-results/results",
|
||||
"originalResultsPrefix": "refactor/.cache/browser/results",
|
||||
"stats": {
|
||||
"startTime": "2026-09-12T03:03:56.604Z",
|
||||
"duration": 68280.7,
|
||||
"expected": 54,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "eslint test/ambilight.test.js test/browser/ambilight.spec.js --no-fix",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"command": "yarn ci:check",
|
||||
"result": "pass",
|
||||
"tests": {
|
||||
"unit": 862,
|
||||
"engineering": 14,
|
||||
"baseline": 79,
|
||||
"mainTotal": 955,
|
||||
"repeatedContractExecutions": 44
|
||||
},
|
||||
"log": "refactor/.cache/ambilight02-ci.log"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"file": "test/ambilight.test.js",
|
||||
"sha256LF": "886a617d1ddb800c9fd639e3a125b9d1788e67a287900b7d6fbf8f6324774c47"
|
||||
},
|
||||
{
|
||||
"file": "test/browser/ambilight.spec.js",
|
||||
"sha256LF": "5035a3a38460d56747b3786299672e4dbde6543bd384fb3c7112e522dd2add7f"
|
||||
},
|
||||
{
|
||||
"file": "test/helpers/ambilight.js",
|
||||
"sha256LF": "7775f147355d2aa08615a82079760924ae151beb7d9bdc94cccfd8195f5b19d8"
|
||||
},
|
||||
{
|
||||
"file": "package.json",
|
||||
"sha256LF": "0d2d5c253b72588e48fe6e68ad7069b70945d8a6bcd6866bbca4516e8299721a"
|
||||
},
|
||||
{
|
||||
"file": "test/browser/server.mjs",
|
||||
"sha256LF": "af513e1e64f875668b6a7cbd060276d85d930e0cc1715577e97907cc26c3b58d"
|
||||
},
|
||||
{
|
||||
"file": "refactor/baselines/ambilight-release.json",
|
||||
"sha256LF": "6acbea40a26397ffa36549fbecb1a12c057cfb3fc932f7cb83181ad5fa37bd2f"
|
||||
}
|
||||
],
|
||||
"logs": [
|
||||
{
|
||||
"file": "refactor/.cache/ambilight02-tests.log",
|
||||
"sha256": "f90963c42d07326e72767e81a9a446891b234ce8e6295192a7bef57ca3be03bf"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ambilight02-browser-final.log",
|
||||
"sha256": "4f1f4ea230fc667f48a1de5a0890abd2f4945342f2760952e1f50d3847e1083b"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ambilight02-lint-final.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"file": "refactor/.cache/ambilight02-ci.log",
|
||||
"sha256": "b265745fbf3347d3c31c94e4a48a2e6006027afeb2239758f68a6976c553d8bd"
|
||||
}
|
||||
],
|
||||
"browserReportSha256": "7bd351759c2003cdc6c4d52dee7bdb4ec24f647ae19a91fa4e6f974b5edcd1c7",
|
||||
"limitations": [
|
||||
"These are historical observations; candidate fixes remain PKG-AMBILIGHT-03",
|
||||
"Controlled zero dimensions prove missing guards, not a specific native exception",
|
||||
"No physical devices, canvas proxy support claim, full consumer type or final packed plugin acceptance"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# PKG-AMBILIGHT-02 历史行为与失败复现
|
||||
|
||||
沿用01两个实际发布及冻结工作区,30项Node测试分别验证频率/暂停/重复start-stop和
|
||||
历史异常路径。对id=0、销毁后逃逸start、取色异常卡住旧handle、空context/零尺寸、
|
||||
取色中stop/destroy重入、设置样式失败和已销毁host晚注册建立可重跑断言。
|
||||
|
||||
浏览器使用原生video、Canvas和RAF;三个核心(实际npm5.1.7、npm5.4.0及候选)、
|
||||
三插件实现、三引擎验证9格取色、暂停/停止保留颜色、切源和destroy(false)残留视图。
|
||||
127.0.0.1页面加载localhost同端口视频构成真实跨origin;手动start捕获原生SecurityError,
|
||||
切回同源且媒体播放后仍复现同一错误,证明历史Canvas污染持续存在。没有拦截网络或
|
||||
模拟像素/原生异常;主动catch用于避免将预期旧缺陷变成浏览器未处理错误。
|
||||
|
||||
Node模拟器的零尺寸被允许,只证明缺少尺寸保护,不声称浏览器一定抛某种异常;真实
|
||||
浏览器结果与源文件/样本/引擎/Core版本由附件保存。默认全局CI仍执行全部必要门槛。
|
||||
专项、三引擎和完整CI结果见 [验证记录](../baselines/ambilight-behavior-validation.json)。
|
||||
|
||||
这些断言固定的是历史问题,03需另加候选应当修复的断言,不能改旧快照让缺陷消失。
|
||||
下一步拆分视图、取色和RAF/销毁归属,保留正常stop/配置/样式/返回行为;类型和最终
|
||||
分发仍归04/06,实际设备与代理能力归05。无生产源码变更、推送或发布。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 220 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 129 / doing 6 / blocked 0 / done 85 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 128 / doing 6 / blocked 0 / done 86 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-AMBILIGHT-01 | artplayer-plugin-ambilight<br>核对包契约与历史用法 | BASE-05 | blur/opacity/frequency/duration、start/stop 与挂载样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done |
|
||||
| PKG-AMBILIGHT-02 | artplayer-plugin-ambilight<br>建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo |
|
||||
| PKG-AMBILIGHT-02 | artplayer-plugin-ambilight<br>建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | done |
|
||||
| PKG-AMBILIGHT-03 | artplayer-plugin-ambilight<br>整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo |
|
||||
| PKG-AMBILIGHT-04 | artplayer-plugin-ambilight<br>迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo |
|
||||
| PKG-AMBILIGHT-05 | artplayer-plugin-ambilight<br>验证新旧核心和组合 | PKG-AMBILIGHT-04, CORE-22 | 原生 video、canvas proxy 的能力边界及销毁无帧循环 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | M | todo |
|
||||
@@ -477,6 +477,7 @@
|
||||
- PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json)
|
||||
- PKG-CHAPTER-05: [记录](changes/2026-09-12-PKG-CHAPTER-05-combinations.md) [记录](baselines/chapter-combinations-checkpoint.json)
|
||||
- PKG-AMBILIGHT-01: [记录](changes/2026-09-12-PKG-AMBILIGHT-01-contract.md) [记录](baselines/ambilight-contract.md) [记录](baselines/ambilight-release.json) [记录](baselines/ambilight-contract-validation.json)
|
||||
- PKG-AMBILIGHT-02: [记录](changes/2026-09-12-PKG-AMBILIGHT-02-tests.md) [记录](baselines/ambilight-behavior-validation.json) [记录](ambilight-validation.md)
|
||||
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
|
||||
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
|
||||
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:Ambilight-02 行为与失败回归
|
||||
|
||||
30项Node复现RAF id零、异常后卡住、销毁/重入/晚注册和初始化回滚问题。54项三引擎/
|
||||
三核心真实媒体验证通过;原生跨域Canvas污染在同源恢复后仍持续,新Canvas能取色的
|
||||
独立对照排除媒体未恢复误判。核心来源为实际npm5.1.7/5.4.0及候选;无skip。主CI955项
|
||||
通过,另44项重复观察;03将拆TS并修复,尚未改生产源码。当前220项:86 done、
|
||||
6 doing、128 todo。见 [实施记录](changes/2026-09-12-PKG-AMBILIGHT-02-tests.md)
|
||||
和 [验证证据](baselines/ambilight-behavior-validation.json)。
|
||||
|
||||
## 最新完成:Ambilight-01 实际发布契约
|
||||
|
||||
冻结npm1.0.0/1.1.0共12成员及工作区源码/声明/demo;真实执行旧CJS.default、新CJS/
|
||||
|
||||
@@ -151,6 +151,6 @@
|
||||
| VAST-TYPE-01 | open / 已复现 | VAST declares an async factory as synchronous and imports an unavailable SDK type dependency | PKG-VAST-04, PKG-VAST-06 |
|
||||
| VAST-LIFE-01 | open / 已复现 | VAST SDK and container lack core destruction ownership and pending work cancellation | PKG-VAST-02, PKG-VAST-03, PKG-VAST-05 |
|
||||
| VAST-DIST-01 | open / 已复现 | Published VAST CommonJS default namespace differs from the current callable distribution | PKG-VAST-04, PKG-VAST-06 |
|
||||
| AMBILIGHT-LIFE-01 | open / 源码/产物事实 | Ambilight sampling and RAF/DOM ownership lack terminal/error handling | PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-05 |
|
||||
| AMBILIGHT-LIFE-01 | open / 已复现 | Ambilight sampling and RAF/DOM ownership lack terminal/error handling | PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-05 |
|
||||
| AMBILIGHT-TYPE-01 | open / 源码/产物事实 | Ambilight declarations disagree with optional factory argument and differ across published generations | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
|
||||
| AMBILIGHT-DIST-01 | open / 已复现 | Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0 | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
|
||||
+7
-3
@@ -3255,7 +3255,7 @@
|
||||
{
|
||||
"id": "AMBILIGHT-LIFE-01",
|
||||
"title": "Ambilight sampling and RAF/DOM ownership lack terminal/error handling",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"owners": [
|
||||
"PKG-AMBILIGHT-02",
|
||||
"PKG-AMBILIGHT-03",
|
||||
@@ -3267,8 +3267,12 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/ambilight-contract.md",
|
||||
"refactor/baselines/ambilight-release.json",
|
||||
"refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md"
|
||||
]
|
||||
"refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md",
|
||||
"refactor/changes/2026-09-12-PKG-AMBILIGHT-02-tests.md",
|
||||
"refactor/baselines/ambilight-behavior-validation.json",
|
||||
"refactor/ambilight-validation.md"
|
||||
],
|
||||
"workspaceState": "30 Node historical tests reproduce RAF id zero, terminal/escaped method, sampling/context, reentrant stop/destroy and setup rollback failures. Native browser sampling of a cross-origin video throws SecurityError and stays tainted after same-origin source recovery. No production fixes in this task."
|
||||
},
|
||||
{
|
||||
"id": "AMBILIGHT-TYPE-01",
|
||||
|
||||
+6
-2
@@ -1481,11 +1481,15 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "M",
|
||||
"deliverable": "跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-AMBILIGHT-02-tests.md",
|
||||
"baselines/ambilight-behavior-validation.json",
|
||||
"ambilight-validation.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-AMBILIGHT-03",
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Preserve historical RAF/canvas behavior separately from candidate fixes.
|
||||
import test from 'node:test'
|
||||
import { ambilightEnvironment, ambilightHistorical } from './helpers/ambilight.js'
|
||||
|
||||
for (const implementation of await ambilightHistorical()) {
|
||||
const label = implementation.name
|
||||
|
||||
test(`Ambilight ${label}: frequency throttles samples and pause/stop preserve last colors`, async () => {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const result = env.factory({ frequency: 2 })(env.art)
|
||||
env.art.emit('ready')
|
||||
env.art.playing = true
|
||||
env.frame(499)
|
||||
assert.equal(env.draws.length, 0)
|
||||
env.frame(500)
|
||||
assert.equal(env.draws.length, 9)
|
||||
const colors = env.parent.children[0].children.map(child => child.style.backgroundColor)
|
||||
env.art.playing = false
|
||||
env.frame(1000)
|
||||
assert.equal(env.draws.length, 9)
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
assert.deepEqual(env.parent.children[0].children.map(child => child.style.backgroundColor), colors)
|
||||
env.art.playing = true
|
||||
result.start()
|
||||
assert.equal(env.draws.length, 18)
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: ordinary start/stop is repeatable and ready can restart an earlier stop`, async () => {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const result = env.factory()(env.art)
|
||||
result.stop()
|
||||
env.art.emit('ready')
|
||||
result.start()
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
result.stop()
|
||||
assert.equal(env.frames.size, 0)
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: special frequency values preserve historical JavaScript division semantics`, async () => {
|
||||
for (const [frequency, samples] of [[0, 0], [-1, 9], [Infinity, 9], [Number.NaN, 9], ['2', 0]]) {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const result = env.factory({ frequency })(env.art)
|
||||
env.art.playing = true
|
||||
env.time(100)
|
||||
result.start()
|
||||
assert.equal(env.draws.length, samples, String(frequency))
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical RAF id zero creates duplicate work and escapes stop`, async () => {
|
||||
const env = await ambilightEnvironment(implementation, { firstFrame: 0 })
|
||||
const result = env.factory()(env.art)
|
||||
result.start()
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 2)
|
||||
result.stop()
|
||||
assert.deepEqual([...env.frames.keys()], [0])
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical destroy leaves its view and escaped start can restart frames`, async () => {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const result = env.factory()(env.art)
|
||||
env.art.emit('ready')
|
||||
env.art.destroy()
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical frame exceptions wedge start until an explicit stop`, async () => {
|
||||
for (const errorType of ['draw', 'read']) {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const result = env.factory()(env.art)
|
||||
const error = Object.assign(new Error(errorType), { name: errorType === 'read' ? 'SecurityError' : 'InvalidStateError' })
|
||||
result.start()
|
||||
env.art.playing = true
|
||||
if (errorType === 'read')
|
||||
env.setReadError(error)
|
||||
else env.setDrawError(error)
|
||||
assert.throws(() => env.frame(100), value => value === error)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.setReadError(null)
|
||||
env.setDrawError(null)
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 0, 'Stale nonzero handle still blocks restart')
|
||||
result.stop()
|
||||
env.time(200)
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical null contexts and zero dimensions reach unguarded sampling`, async () => {
|
||||
const missing = await ambilightEnvironment(implementation, { noContext: true })
|
||||
const result = missing.factory()(missing.art)
|
||||
missing.art.playing = true
|
||||
missing.time(100)
|
||||
assert.throws(result.start, error => error.name === 'TypeError')
|
||||
assert.equal(missing.parent.children.length, 2)
|
||||
const zero = await ambilightEnvironment(implementation)
|
||||
zero.video.videoWidth = zero.video.videoHeight = 0
|
||||
zero.art.playing = true
|
||||
zero.time(100)
|
||||
const zeroResult = zero.factory()(zero.art)
|
||||
zeroResult.start()
|
||||
assert.equal(zero.draws.length, 9, 'Recorder accepts zero area; this proves missing guard, not browser acceptance')
|
||||
assert.equal(zero.draws[0][3], 0)
|
||||
assert.equal(zero.draws[0][4], 0)
|
||||
zeroResult.stop()
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical stop/destroy during sampling queues a new frame after cleanup`, async () => {
|
||||
for (const operation of ['stop', 'destroy']) {
|
||||
let result
|
||||
let first = true
|
||||
const env = await ambilightEnvironment(implementation, { onDraw() {
|
||||
if (!first)
|
||||
return
|
||||
first = false
|
||||
if (operation === 'stop')
|
||||
result.stop()
|
||||
else env.art.destroy()
|
||||
} })
|
||||
result = env.factory()(env.art)
|
||||
result.start()
|
||||
env.art.playing = true
|
||||
env.frame(100)
|
||||
assert.equal(env.draws.length, 9)
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical setup failure retains an inserted view without a cleanup handle`, async () => {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
const failure = new Error('style setup failed')
|
||||
env.art.constructor.utils.setStyles = () => {
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
assert.equal(env.listeners.size, 0)
|
||||
})
|
||||
|
||||
test(`Ambilight ${label}: historical late attachment to a destroyed host still allocates and starts`, async () => {
|
||||
const env = await ambilightEnvironment(implementation)
|
||||
env.art.destroy()
|
||||
const result = env.factory()(env.art)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
result.start()
|
||||
assert.equal(env.frames.size, 1)
|
||||
result.stop()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { transform } from 'esbuild'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { ambilightHistorical } from '../helpers/ambilight.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = await Promise.all((await ambilightHistorical()).map(async item => ({ ...item, code: item.format === 'source' ? (await transform(item.source, { format: 'cjs', target: 'es2020' })).code : item.source })))
|
||||
|
||||
async function setup(page, core, implementation, testInfo, crossOrigin = false) {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.ambilightFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate((crossOrigin) => {
|
||||
const url = new URL('/test/pattern.mp4', location.href)
|
||||
if (crossOrigin)
|
||||
url.hostname = 'localhost'
|
||||
window.createPlayer(url.href)
|
||||
}, crossOrigin)
|
||||
await expect.poll(() => page.evaluate(() => window.art.template.$video.videoWidth)).toBeGreaterThan(0)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.currentTime > 0)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.plugins.add(window.ambilightFactory({ frequency: Infinity }))
|
||||
window.ambilight = window.art.plugins.artplayerPluginAmbilight
|
||||
})
|
||||
await testInfo.attach('ambilight-inputs', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, sha256: hash(implementation.code), crossOrigin, media: '/test/pattern.mp4', canvas: 'native, no pixel or RAF stubs' }) })
|
||||
}
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await testInfo.attach('ambilight-state', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ version: window.Artplayer?.version, source: window.art?.url, width: window.art?.template.$video.videoWidth, currentTime: window.art?.currentTime, errors: window.ambilightErrors, grids: document.querySelectorAll('.artplayer-plugin-ambilight').length }))) })
|
||||
await page.evaluate(() => {
|
||||
// Historical escaped methods intentionally restart work; cleanup follows assertions.
|
||||
window.ambilight?.stop()
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
for (const core of ['published-5.1.7', 'published', 'candidate']) {
|
||||
for (const implementation of implementations) {
|
||||
const label = `${core} / ${implementation.name}`
|
||||
test(`${label}: native canvas colors, pause, source switch and historical retained destroy view`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo)
|
||||
await page.evaluate(() => window.ambilight.start())
|
||||
await expect.poll(() => page.evaluate(() => [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].filter(node => /^rgb\(/.test(node.style.backgroundColor)).length)).toBe(9)
|
||||
const layout = await page.evaluate(() => {
|
||||
const grid = document.querySelector('.artplayer-plugin-ambilight')
|
||||
return { parentMatches: grid.parentNode === window.art.template.$video.parentNode, nextMatches: grid.nextSibling === window.art.template.$video, columns: getComputedStyle(grid).gridTemplateColumns.split(' ').length, zIndex: getComputedStyle(grid).zIndex }
|
||||
})
|
||||
expect(layout).toEqual({ parentMatches: true, nextMatches: true, columns: 3, zIndex: '9' })
|
||||
await page.click('#pause')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(false)
|
||||
const before = await page.evaluate(() => [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].map(node => node.style.backgroundColor))
|
||||
const stopped = await page.evaluate(() => {
|
||||
const returned = window.ambilight.stop()
|
||||
return { returned: returned === undefined, colors: [...document.querySelectorAll('.artplayer-plugin-ambilight > div')].map(node => node.style.backgroundColor) }
|
||||
})
|
||||
expect(stopped).toEqual({ returned: true, colors: before })
|
||||
await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4'))
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.template.$video.videoWidth > 0)).toBe(true)
|
||||
await page.evaluate(() => window.ambilight.start())
|
||||
expect(await page.evaluate(() => window.art.template.$video.currentSrc.endsWith('/assets/sample/video.mp4'))).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(await page.locator('.artplayer-plugin-ambilight').count()).toBe(1)
|
||||
})
|
||||
|
||||
test(`${label}: native cross-origin SecurityError persists after same-origin recovery in historical canvas`, async ({ page }, testInfo) => {
|
||||
await setup(page, core, implementation, testInfo, true)
|
||||
const capture = () => page.evaluate(() => {
|
||||
window.ambilight.stop()
|
||||
try {
|
||||
window.ambilight.start()
|
||||
return null
|
||||
}
|
||||
catch (error) {
|
||||
window.ambilightErrors = [...(window.ambilightErrors || []), error.name]
|
||||
return error.name
|
||||
}
|
||||
})
|
||||
expect(await capture()).toBe('SecurityError')
|
||||
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4'))
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing && window.art.template.$video.currentSrc.startsWith(location.origin))).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
try {
|
||||
context.drawImage(window.art.template.$video, 0, 0, 1, 1)
|
||||
context.getImageData(0, 0, 1, 1)
|
||||
return true
|
||||
}
|
||||
catch { return false }
|
||||
})).toBe(true)
|
||||
expect(await capture()).toBe('SecurityError')
|
||||
expect(await page.evaluate(() => window.art.currentTime)).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user