mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
import { expect, test } from './fixtures.js'
|
|
|
|
async function setup(page, core) {
|
|
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
|
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await page.locator('#play').click()
|
|
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
|
await page.locator('#pause').click()
|
|
await expect.poll(() => page.evaluate(() => window.art.video.paused && !window.art.video.seeking)).toBe(true)
|
|
}
|
|
|
|
for (const core of ['published', 'candidate']) {
|
|
for (const mode of ['remove', 'destroy', 'source']) {
|
|
test(`${core}: progress click reentry through ${mode} cannot continue an obsolete seek`, async ({ page }) => {
|
|
await setup(page, core)
|
|
const result = await page.evaluate((mode) => {
|
|
const art = window.art
|
|
let seeks = 0
|
|
let bars = 0
|
|
art.on('seek', () => seeks++)
|
|
art.on('setBar', (type) => {
|
|
if (type !== 'played' || bars++)
|
|
return
|
|
if (mode === 'remove')
|
|
art.controls.remove('progress')
|
|
else if (mode === 'destroy')
|
|
art.destroy()
|
|
else
|
|
art.url = '/test/pattern.mp4?progress=reentrant'
|
|
})
|
|
const progress = art.template.$progress
|
|
progress.dispatchEvent(new MouseEvent('click', { clientX: progress.getBoundingClientRect().left + progress.clientWidth / 2 }))
|
|
const result = { seeks, bars }
|
|
art.destroy()
|
|
return result
|
|
}, mode)
|
|
expect(result).toEqual({ seeks: core === 'published' ? 1 : 0, bars: 1 })
|
|
})
|
|
}
|
|
|
|
test(`${core}: real progress click keeps setBar before seek and clamps the edges`, async ({ page }) => {
|
|
await setup(page, core)
|
|
const rect = await page.evaluate(() => {
|
|
const art = window.art
|
|
window.progressOrder = []
|
|
art.on('setBar', (type, percent) => {
|
|
if (type === 'played')
|
|
window.progressOrder.push(['setBar', percent])
|
|
})
|
|
art.on('seek', () => window.progressOrder.push(['seek']))
|
|
const rect = art.template.$progress.getBoundingClientRect()
|
|
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
})
|
|
await page.mouse.click(rect.x, rect.y)
|
|
await expect.poll(() => page.evaluate(() => window.art.video.seeking)).toBe(false)
|
|
expect(await page.evaluate(() => window.art.currentTime / window.art.duration)).toBeCloseTo(0.5, 1)
|
|
const result = await page.evaluate(() => {
|
|
const art = window.art
|
|
const firstSeek = window.progressOrder.findIndex(item => item[0] === 'seek')
|
|
const previous = window.progressOrder[firstSeek - 1]
|
|
const progress = art.template.$progress
|
|
progress.dispatchEvent(new MouseEvent('click', { clientX: progress.getBoundingClientRect().left - 100 }))
|
|
const start = art.currentTime
|
|
progress.dispatchEvent(new MouseEvent('click', { clientX: progress.getBoundingClientRect().right + 100 }))
|
|
const end = art.currentTime
|
|
const duration = art.duration
|
|
art.destroy()
|
|
return { previous, start, end, duration }
|
|
})
|
|
expect(result.previous[0]).toBe('setBar')
|
|
expect(result.previous[1]).toBeCloseTo(0.5, 1)
|
|
expect(result.start).toBe(0)
|
|
expect(result.end).toBeCloseTo(result.duration, 1)
|
|
})
|
|
|
|
test(`${core}: a progress drag ends when its source is replaced and can restart on the new source`, async ({ page }) => {
|
|
await setup(page, core)
|
|
const stale = await page.evaluate(() => {
|
|
const art = window.art
|
|
window.dragSeeks = 0
|
|
art.on('seek', () => window.dragSeeks++)
|
|
const progress = art.template.$progress
|
|
progress.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
|
art.url = '/test/pattern.mp4?progress=new-source'
|
|
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: progress.getBoundingClientRect().left + progress.clientWidth / 2 }))
|
|
return window.dragSeeks
|
|
})
|
|
expect(stale).toBe(core === 'published' ? 1 : 0)
|
|
await expect.poll(() => page.evaluate(() => window.art.video.readyState >= 2)).toBe(true)
|
|
expect(await page.evaluate(() => {
|
|
const art = window.art
|
|
const progress = art.template.$progress
|
|
progress.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
|
art.emit('document:mousemove', new MouseEvent('mousemove', { clientX: progress.getBoundingClientRect().left + progress.clientWidth / 2 }))
|
|
art.emit('document:mouseup', new MouseEvent('mouseup'))
|
|
const seeks = window.dragSeeks
|
|
art.destroy()
|
|
return seeks
|
|
})).toBe(stale + 1)
|
|
})
|
|
|
|
test(`${core}: a caller-owned media Blob URL remains readable after switching and destroying the player`, async ({ page, browserName }, testInfo) => {
|
|
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
|
await page.evaluate(async () => {
|
|
window.blobCapabilities = []
|
|
let blob
|
|
let fallback
|
|
for (const extension of ['mp4', 'webm']) {
|
|
const candidate = await (await fetch(`/test/pattern.${extension}`)).blob()
|
|
fallback ||= candidate
|
|
const url = URL.createObjectURL(candidate)
|
|
const video = document.createElement('video')
|
|
const capability = await new Promise((resolve) => {
|
|
video.onloadedmetadata = () => resolve({ extension, width: video.videoWidth, error: 0 })
|
|
video.onerror = () => resolve({ extension, width: video.videoWidth, error: video.error.code })
|
|
video.src = url
|
|
video.load()
|
|
})
|
|
window.blobCapabilities.push(capability)
|
|
video.onloadedmetadata = null
|
|
video.onerror = null
|
|
video.removeAttribute('src')
|
|
video.load()
|
|
URL.revokeObjectURL(url)
|
|
if (!capability.error) {
|
|
blob = candidate
|
|
break
|
|
}
|
|
}
|
|
window.blobPlayable = !!blob
|
|
blob ||= fallback
|
|
window.sharedBlobSize = blob.size
|
|
window.sharedBlobUrl = URL.createObjectURL(blob)
|
|
window.borrowedRevocations = []
|
|
const original = URL.revokeObjectURL
|
|
URL.revokeObjectURL = function (url) {
|
|
window.borrowedRevocations.push(url)
|
|
return original.call(this, url)
|
|
}
|
|
window.createPlayer(window.blobPlayable ? window.sharedBlobUrl : '/test/pattern.mp4')
|
|
})
|
|
const capabilities = await page.evaluate(() => window.blobCapabilities)
|
|
await testInfo.attach('native-blob-capabilities', { contentType: 'application/json', body: JSON.stringify(capabilities) })
|
|
if (!capabilities.some(item => item.error === 0)) {
|
|
expect(browserName).toBe('webkit')
|
|
expect(capabilities).toEqual([{ extension: 'mp4', width: 0, error: 4 }, { extension: 'webm', width: 0, error: 4 }])
|
|
}
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
const result = await page.evaluate(async () => {
|
|
const art = window.art
|
|
// The Windows WebKit runtime cannot decode either Blob fixture natively.
|
|
// Still exercise actual URL assignment/replacement and byte ownership there.
|
|
if (!window.blobPlayable)
|
|
art.url = window.sharedBlobUrl
|
|
const assigned = art.video.src === window.sharedBlobUrl
|
|
await art.switchUrl('/test/pattern.mp4?borrowed=replaced')
|
|
const read = async () => {
|
|
try {
|
|
return (await (await fetch(window.sharedBlobUrl)).blob()).size
|
|
}
|
|
catch (error) {
|
|
return error.name
|
|
}
|
|
}
|
|
const afterSwitch = await read()
|
|
art.destroy()
|
|
const afterDestroy = await read()
|
|
const revoked = window.borrowedRevocations.filter(url => url === window.sharedBlobUrl).length
|
|
URL.revokeObjectURL(window.sharedBlobUrl)
|
|
return { afterSwitch, afterDestroy, revoked, size: window.sharedBlobSize, assigned, decodedBlob: window.blobPlayable }
|
|
})
|
|
expect(result.size).toBeGreaterThan(100)
|
|
expect(result.assigned).toBe(true)
|
|
await testInfo.attach('borrowed-url-evidence', { contentType: 'application/json', body: JSON.stringify(result) })
|
|
expect(result.revoked).toBe(core === 'published' ? 1 : 0)
|
|
expect(result.afterSwitch).toBe(core === 'published' ? 'TypeError' : result.size)
|
|
expect(result.afterDestroy).toBe(core === 'published' ? 'TypeError' : result.size)
|
|
})
|
|
|
|
test(`${core}: quality selectors retain default labels, source position and empty-array behavior`, async ({ page, browserName }, testInfo) => {
|
|
await setup(page, core)
|
|
await page.evaluate(() => {
|
|
const art = window.art
|
|
window.qualityNativeEvents = []
|
|
for (const key of ['currentTime', 'playbackRate', 'src']) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, key)
|
|
Object.defineProperty(art.video, key, {
|
|
configurable: true,
|
|
get() { return descriptor.get.call(this) },
|
|
set(value) {
|
|
window.qualityNativeEvents.push({ key, value, time: this.currentTime, seeking: this.seeking, source: this.currentSrc })
|
|
descriptor.set.call(this, value)
|
|
},
|
|
})
|
|
}
|
|
for (const event of ['loadedmetadata', 'seeking', 'seeked', 'canplay', 'ratechange', 'pause', 'playing'])
|
|
art.video.addEventListener(event, () => window.qualityNativeEvents.push({ event, time: art.currentTime, seeking: art.video.seeking, source: art.video.currentSrc }))
|
|
art.playbackRate = 1.5
|
|
art.currentTime = 2
|
|
window.qualityRestarts = []
|
|
art.on('restart', url => window.qualityRestarts.push(url))
|
|
window.qualityOptions = [
|
|
{ html: 'Quality A', url: '/test/pattern.mp4?quality=A' },
|
|
{ html: 'Quality B', url: '/test/pattern.mp4?quality=B', default: true },
|
|
]
|
|
art.quality = window.qualityOptions
|
|
})
|
|
await expect.poll(() => page.evaluate(() => Math.abs(window.art.currentTime - 2) < 0.05 && !window.art.video.seeking)).toBe(true)
|
|
await expect(page.locator('.art-control-quality .art-selector-value')).toHaveText('Quality B')
|
|
await page.locator('.art-control-quality').hover()
|
|
await page.locator('.art-control-quality .art-selector-item').filter({ hasText: 'Quality A' }).click()
|
|
await expect.poll(() => page.evaluate(() => window.art.url.includes('quality=A'))).toBe(true)
|
|
await expect.poll(() => page.evaluate(() => window.qualityRestarts)).toEqual(['/test/pattern.mp4?quality=A'])
|
|
await expect.poll(() => page.evaluate(() => window.art.video.readyState >= 3 && !window.art.video.seeking)).toBe(true)
|
|
await expect(page.locator('.art-control-quality .art-selector-value')).toHaveText('Quality A')
|
|
await testInfo.attach('quality-native-events', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => window.qualityNativeEvents)) })
|
|
const restoredTime = await page.evaluate(() => window.art.currentTime)
|
|
if (core === 'published' && browserName === 'webkit')
|
|
expect(restoredTime).toBeLessThan(0.05)
|
|
else
|
|
expect(restoredTime).toBeCloseTo(2, 1)
|
|
expect(await page.evaluate(() => window.art.playbackRate)).toBe(1.5)
|
|
expect(await page.evaluate(() => window.qualityOptions.map(item => !!item.default))).toEqual([true, false])
|
|
const empty = await page.evaluate(() => {
|
|
const art = window.art
|
|
art.quality = []
|
|
const result = { text: art.controls.quality.textContent, read: art.quality, hasGetter: !!Object.getOwnPropertyDescriptor(art, 'quality').get }
|
|
art.destroy()
|
|
return result
|
|
})
|
|
expect(empty).toEqual({ text: '', read: undefined, hasGetter: false })
|
|
})
|
|
}
|