mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 04:01:20 -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
@@ -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