test(ambilight): [PKG-AMBILIGHT-02] reproduce historical frame and native canvas failures

This commit is contained in:
Harvey Zhao committed 2026-09-12 11:09:40 +08:00
1 parent 06032a6906
commit 43f0e9a8c6
11 files changed
+435 -10

No files matched your search

+3 -2
View File
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/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": {
+13
View File
@@ -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
View File
@@ -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)
+9
View File
@@ -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/
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+168
View File
@@ -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()
})
}
+97
View File
@@ -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)
})
}
}