Files
ArtPlayer/test/browser/initialization.spec.js
T

273 lines
12 KiB
JavaScript

import { expect, test } from './fixtures.js'
for (const removeHtml of [true, false]) {
test(`candidate: proxy-time destroy cleans the mounting template with removeHtml=${removeHtml}`, async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const result = await page.evaluate((removeHtml) => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let calls = 0
let visibleTemplate
let destroyedDOM
const proxy = document.createElement('video')
proxy.className = 'owned-by-caller'
const art = new Artplayer({ container, url: '', proxy(owner) {
visibleTemplate = Object.hasOwn(owner, 'template')
owner.on('destroy', () => {
calls++
destroyedDOM = { nodes: container.childElementCount, retained: !!container.querySelector('.art-destroy') }
})
owner.destroy(removeHtml)
return proxy
} })
art.destroy(!removeHtml)
const result = {
visibleTemplate,
calls,
destroyed: art.isDestroy,
registered: Artplayer.instances.includes(art),
hasEvents: Object.hasOwn(art, 'events'),
nodes: container.childElementCount,
retained: !!container.querySelector('.art-destroy'),
destroyedDOM,
proxyUntouched: proxy.parentNode === null && proxy.className === 'owned-by-caller',
}
const next = new Artplayer({ container, url: '' })
result.reusable = Artplayer.instances.includes(next)
next.destroy()
return result
}, removeHtml)
const dom = { nodes: removeHtml ? 0 : 1, retained: !removeHtml }
expect(result).toEqual({ visibleTemplate: false, calls: 1, destroyed: true, registered: false, hasEvents: false, ...dom, destroyedDOM: dom, proxyUntouched: true, reusable: true })
})
}
test('candidate: player descriptor order and constructor/static surfaces match the published core', async ({ page }) => {
const results = []
for (const core of ['published', 'candidate']) {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
results.push(await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
const art = new Artplayer({ container, url: '', muted: true })
const describe = (object, names) => names.map((name) => {
const property = Object.getOwnPropertyDescriptor(object, name)
return [name, property.enumerable, property.configurable, property.writable ?? null, typeof property.get, typeof property.set, typeof property.value]
})
const properties = Object.getOwnPropertyNames(art).filter((name) => {
const property = Object.getOwnPropertyDescriptor(art, name)
return !property.configurable && !property.enumerable
})
const result = {
arity: Artplayer.length,
playerArity: art.player.constructor.length,
playerKeys: Object.keys(art.player),
descriptors: describe(art, properties),
prototype: describe(Artplayer.prototype, Object.getOwnPropertyNames(Artplayer.prototype).filter(name => name !== 'constructor')),
statics: describe(Artplayer, Object.getOwnPropertyNames(Artplayer).filter(name => !['length', 'name', 'prototype'].includes(name))),
state: [art.type, art.muted, art.volume, art.currentTime, art.played, art.loaded, art.playing, art.state],
}
art.destroy()
return result
}))
}
expect(results[1]).toEqual(results[0])
expect(results[1].arity).toBe(2)
expect(results[1].playerArity).toBe(1)
expect(results[1].playerKeys).toEqual([])
})
test('candidate: proxy-time destroy listener can mount a replacement without the old constructor overwriting it', async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let replacement
const unused = document.createElement('video')
const old = new Artplayer({ container, url: '', proxy(owner) {
owner.on('destroy', () => {
replacement = new Artplayer({ container, url: '' })
})
owner.destroy()
return unused
} })
const result = {
oldDestroyed: old.isDestroy,
registry: Artplayer.instances.length === 1 && Artplayer.instances[0] === replacement,
player: container.querySelector('.art-video-player') === replacement.template.$player,
video: container.querySelector('video') === replacement.video,
artId: container.dataset.artId === String(replacement.id),
unusedDetached: unused.parentNode === null,
}
old.destroy()
result.stillRegistered = Artplayer.instances.includes(replacement)
replacement.destroy()
return result
})
expect(result).toEqual({ oldDestroyed: true, registry: true, player: true, video: true, artId: true, unusedDetached: true, stillRegistered: true })
})
for (const core of ['published', 'candidate']) {
test(`${core}: subclasses retain receiver identity, initialization timing and static inheritance`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let subclassDefaults = 0
class Extended extends Artplayer {
static get option() {
subclassDefaults++
return super.option
}
constructor(option, ready) {
super(option, ready)
this.extension = 'after-super'
}
}
Extended.NOTICE_TIME = 321
let pluginShape
let readyShape
const art = new Extended({ container, url: '', plugins: [function (owner) {
pluginShape = this === owner && owner instanceof Extended && !Object.hasOwn(owner, 'extension')
return { name: 'subclass-plugin' }
}] }, function (owner) {
readyShape = this === owner && owner instanceof Extended && owner.extension === 'after-super'
})
const chained = art.emit('ready') === art
const result = { pluginShape, readyShape, chained, subclassDefaults, registry: Artplayer.instances.includes(art), prototype: Object.getPrototypeOf(art) === Extended.prototype, constant: art.notice.art.constructor.NOTICE_TIME, inheritedInstances: Extended.instances === Artplayer.instances }
art.destroy()
return result
})
expect(result).toEqual({ pluginShape: true, readyShape: true, chained: true, subclassDefaults: 0, registry: true, prototype: true, constant: 321, inheritedInstances: true })
})
test(`${core}: omitted unused SSR nodes stay nullable without an eager template rejection`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
container.innerHTML = Artplayer.html
container.querySelector('.art-danmuku').remove()
document.body.append(container)
const art = new Artplayer({ container, url: '', useSSR: true })
const result = { missing: art.template.$danmuku === null, query: art.query('.missing-node') === null, mounted: container.querySelector('.art-video-player') === art.template.$player }
art.destroy()
return result
})
expect(result).toEqual({ missing: true, query: true, mounted: true })
})
test(`${core}: teardown during capability detection stops remaining player installers`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const key = 'WebKitPlaybackTargetAvailabilityEvent'
const previous = Object.getOwnPropertyDescriptor(window, key)
const container = document.createElement('div')
document.body.append(container)
let captured
let reads = 0
Object.defineProperty(window, key, { configurable: true, get() {
reads++
if (captured && !captured.isDestroy)
captured.destroy()
return undefined
} })
try {
const art = new Artplayer({ container, url: '', proxy(owner) {
captured = owner
return document.createElement('video')
} })
return {
reads,
returned: captured === art,
destroyed: art.isDestroy,
registered: Artplayer.instances.includes(art),
airplay: Object.hasOwn(art, 'airplay'),
quality: Object.hasOwn(art, 'quality'),
thumbnails: Object.hasOwn(art, 'thumbnails'),
controls: Object.hasOwn(art, 'controls'),
nodes: container.childElementCount,
}
}
finally {
if (previous)
Object.defineProperty(window, key, previous)
else
delete window[key]
}
})
expect(result).toEqual({ reads: 1, returned: true, destroyed: true, registered: core === 'published', airplay: core === 'published', quality: core === 'published', thumbnails: core === 'published', controls: core === 'published', nodes: 0 })
})
test(`${core}: destroy during initial native volume assignment stops later construction`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(async () => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
const calls = []
let captured
let thrown
let returned
try {
returned = new Artplayer({
container,
url: '',
proxy(art) {
captured = art
const video = document.createElement('video')
const volume = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume')
Object.defineProperty(video, 'volume', {
get() { return volume.get.call(this) },
set(value) {
calls.push('volume')
volume.set.call(this, value)
if (!art.isDestroy) {
art.destroy()
calls.push('destroyed')
}
},
})
return video
},
layers: [{ name: 'later', html: 'later' }],
plugins: [(art) => {
calls.push(`plugin:${art.isDestroy}`)
return { name: 'later' }
}],
})
}
catch (error) { thrown = error.message }
if (!captured)
throw new Error(`Initialization fixture did not reach proxy: ${thrown}`)
await new Promise(resolve => setTimeout(resolve, 0))
return {
calls,
thrown: thrown ?? null,
returned: returned === captured,
destroyed: captured.isDestroy,
registered: Artplayer.instances.includes(captured),
hasLayers: Object.hasOwn(captured, 'layers'),
hasPlugins: Object.hasOwn(captured, 'plugins'),
nodes: container.childElementCount,
listeners: captured.events.destroyEvents.size,
}
})
if (core === 'candidate') {
expect(result).toEqual({ calls: ['volume', 'destroyed'], thrown: null, returned: true, destroyed: true, registered: false, hasLayers: false, hasPlugins: false, nodes: 0, listeners: 0 })
}
else {
expect(result.calls).toContain('plugin:true')
expect(result.registered).toBe(true)
expect(result.hasLayers).toBe(true)
expect(result.hasPlugins).toBe(true)
}
})
}