mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 12:46:15 -08:00
187 lines
10 KiB
JavaScript
187 lines
10 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/danmuku-release.json', import.meta.url), 'utf8'))
|
|
const release = baseline.release
|
|
const mediaSha256 = hash(fs.readFileSync(new URL('./media/pattern.mp4', import.meta.url)))
|
|
const implementations = new Map()
|
|
assert.equal(release.version, '5.3.0')
|
|
|
|
test.beforeAll(async () => {
|
|
const archive = await ensureArchive(release)
|
|
for (const format of ['main', 'legacy']) {
|
|
const member = `package/${release.manifest[format].replace(/^\.\//u, '')}`
|
|
const bytes = readMember(archive, member)
|
|
assert.equal(hash(bytes), release.files[member])
|
|
implementations.set(format, { member, sha256: hash(bytes), code: bytes.toString() })
|
|
}
|
|
})
|
|
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
await page.evaluate(() => {
|
|
if (window.loadObserverFrame !== undefined)
|
|
cancelAnimationFrame(window.loadObserverFrame)
|
|
if (window.loadArt && !window.loadArt.isDestroy)
|
|
window.loadArt.destroy()
|
|
})
|
|
await testInfo.attach('danmuku-load-final-observations', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({
|
|
evidence: window.loadEvidence,
|
|
destroyed: window.loadArt?.isDestroy,
|
|
retainedPlayerRoots: document.querySelectorAll('#danmuku-load-player .art-video-player').length,
|
|
retainedExternalSettings: document.querySelectorAll('#danmuku-load-mount .artplayer-plugin-danmuku').length,
|
|
}))) })
|
|
})
|
|
|
|
for (const format of ['main', 'legacy']) {
|
|
test(`published core / Danmuku 5.3.0 ${format}: 300-comment native load and frame/heap observation`, async ({ page }, testInfo) => {
|
|
const external = []
|
|
const origin = new URL(testInfo.project.use.baseURL).origin
|
|
await page.route('**/*', (route) => {
|
|
const url = new URL(route.request().url())
|
|
if (url.origin !== origin) {
|
|
external.push(url.href)
|
|
return route.abort()
|
|
}
|
|
return route.continue()
|
|
})
|
|
await page.goto('/test/player.html?core=published')
|
|
await page.evaluate(() => {
|
|
window.loadEvidence = { workers: [], loaded: [], visible: [], frameTimes: [], filtered: 0, heaps: [], failures: [], destroyEvents: 0 }
|
|
window.readLoadHeap = (phase) => {
|
|
const memory = performance.memory
|
|
const snapshot = memory && [memory.usedJSHeapSize, memory.totalJSHeapSize, memory.jsHeapSizeLimit].every(Number.isFinite)
|
|
? { usedJSHeapSize: memory.usedJSHeapSize, totalJSHeapSize: memory.totalJSHeapSize, jsHeapSizeLimit: memory.jsHeapSizeLimit }
|
|
: null
|
|
window.loadEvidence.heaps.push({ phase, timestamp: performance.now(), memory: snapshot })
|
|
}
|
|
window.readLoadHeap('before-plugin-script')
|
|
const NativeWorker = window.Worker
|
|
window.Worker = class ObservedLoadWorker extends NativeWorker {
|
|
constructor(url, options) {
|
|
super(url, options)
|
|
this.observation = { url: String(url), posts: 0, replies: 0, terminated: 0, errors: [] }
|
|
window.loadEvidence.workers.push(this.observation)
|
|
this.addEventListener('message', () => this.observation.replies++)
|
|
this.addEventListener('error', event => this.observation.errors.push(event.message))
|
|
}
|
|
|
|
postMessage(message, ...options) {
|
|
this.observation.posts++
|
|
return super.postMessage(message, ...options)
|
|
}
|
|
|
|
terminate() {
|
|
this.observation.terminated++
|
|
return super.terminate()
|
|
}
|
|
}
|
|
// Independent observer: never replace the plugin's RAF or mutate its clock.
|
|
function frame(timestamp) {
|
|
if (window.loadArt?.playing)
|
|
window.loadEvidence.frameTimes.push(timestamp)
|
|
window.loadObserverFrame = requestAnimationFrame(frame)
|
|
}
|
|
window.loadObserverFrame = requestAnimationFrame(frame)
|
|
})
|
|
const implementation = implementations.get(format)
|
|
await page.addScriptTag({ content: implementation.code })
|
|
await testInfo.attach('danmuku-load-inputs', { contentType: 'application/json', body: JSON.stringify({ core: 'published', coreVersion: await page.evaluate(() => window.Artplayer.version), pluginVersion: release.version, format, member: implementation.member, sha256: implementation.sha256, integrity: release.integrity, mediaSha256, comments: 300, modes: [0, 1, 2], scheduledTime: 0.5, heatmap: false, scope: 'Single bounded native sample; independent RAF observer and optional performance.memory readings. No forced GC, leak diagnosis, cross-browser heap equivalence or performance threshold.' }) })
|
|
await page.evaluate(async () => {
|
|
const container = document.querySelector('.player')
|
|
container.id = 'danmuku-load-player'
|
|
container.style.cssText = 'width:640px;height:360px'
|
|
const mount = document.createElement('div')
|
|
mount.id = 'danmuku-load-mount'
|
|
mount.style.cssText = 'width:640px;height:36px;background:#333'
|
|
container.after(mount)
|
|
const art = window.loadArt = new window.Artplayer({ container, url: '/test/pattern.mp4?case=danmuku-load-300', muted: true })
|
|
art.on('artplayerPluginDanmuku:loaded', queue => window.loadEvidence.loaded.push({ count: queue.length, ids: queue.map(item => item.id) }))
|
|
art.on('artplayerPluginDanmuku:visible', item => window.loadEvidence.visible.push({ id: item.id, mode: item.mode, mediaTime: art.currentTime, timestamp: performance.now() }))
|
|
art.on('artplayerPluginDanmuku:error', error => window.loadEvidence.failures.push(String(error)))
|
|
art.on('artplayerPluginDanmuku:destroy', () => window.loadEvidence.destroyEvents++)
|
|
art.plugins.add(window.artplayerPluginDanmuku({
|
|
danmuku: [],
|
|
mount,
|
|
heatmap: false,
|
|
speed: 8,
|
|
fontSize: 18,
|
|
margin: [10, 10],
|
|
antiOverlap: true,
|
|
filter(item) {
|
|
window.loadEvidence.filtered++
|
|
return item.id.startsWith('load-')
|
|
},
|
|
}))
|
|
const plugin = window.loadPlugin = art.plugins.artplayerPluginDanmuku
|
|
const input = Array.from({ length: 300 }, (_, index) => ({ id: `load-${String(index).padStart(3, '0')}`, text: `Load ${index}`, mode: index % 3, time: 0.5 }))
|
|
await plugin.load(input)
|
|
window.readLoadHeap('after-300-loaded')
|
|
document.querySelector('#play').onclick = () => art.play()
|
|
document.querySelector('#pause').onclick = () => art.pause()
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.loadArt.isReady && window.loadArt.template.$video.videoWidth > 0)).toBe(true)
|
|
const loaded = await page.evaluate(() => ({ latest: window.loadEvidence.loaded.at(-1), filtered: window.loadEvidence.filtered }))
|
|
expect(loaded.latest.count).toBe(300)
|
|
expect(new Set(loaded.latest.ids).size).toBe(300)
|
|
expect(loaded.filtered).toBe(300)
|
|
await page.click('#play')
|
|
// A state deadline, not a performance pass/fail budget or a fixed sleep.
|
|
await expect.poll(() => page.evaluate(() => window.loadArt.currentTime), { timeout: 15000 }).toBeGreaterThanOrEqual(4)
|
|
await page.click('#pause')
|
|
await expect.poll(() => page.evaluate(() => window.loadArt.template.$video.paused && window.loadPlugin.isStop)).toBe(true)
|
|
const sample = await page.evaluate(() => {
|
|
cancelAnimationFrame(window.loadObserverFrame)
|
|
window.loadObserverFrame = undefined
|
|
window.readLoadHeap('paused-after-four-media-seconds')
|
|
const art = window.loadArt
|
|
const rect = art.template.$player.getBoundingClientRect()
|
|
const nodes = [...art.template.$danmuku.children].map((node) => {
|
|
const position = node.getBoundingClientRect()
|
|
const style = getComputedStyle(node)
|
|
return { id: node.dataset.id, state: node.dataset.state, mode: node.dataset.mode, text: node.textContent, x: position.x - rect.x, y: position.y - rect.y, width: position.width, height: position.height, visibility: style.visibility, display: style.display }
|
|
})
|
|
const times = window.loadEvidence.frameTimes
|
|
const frameIntervals = times.slice(1).map((timestamp, index) => timestamp - times[index])
|
|
const sorted = [...frameIntervals].sort((a, b) => a - b)
|
|
return {
|
|
coreVersion: window.Artplayer.version,
|
|
mediaTime: art.currentTime,
|
|
video: { width: art.template.$video.videoWidth, height: art.template.$video.videoHeight, duration: art.duration },
|
|
loadedCount: window.loadEvidence.loaded.at(-1).count,
|
|
visibleEvents: window.loadEvidence.visible.length,
|
|
uniqueVisible: new Set(window.loadEvidence.visible.map(item => item.id)).size,
|
|
nodeCount: nodes.length,
|
|
visibleNodeCount: nodes.filter(node => node.visibility === 'visible' && node.display !== 'none').length,
|
|
hiddenNodeCount: nodes.filter(node => node.visibility === 'hidden' || node.display === 'none').length,
|
|
nodes,
|
|
frameTimes: times,
|
|
frameIntervals,
|
|
frameSummary: { count: frameIntervals.length, median: sorted[Math.floor(sorted.length / 2)] ?? null, p95: sorted[Math.floor(sorted.length * 0.95)] ?? null, max: sorted.at(-1) ?? null },
|
|
heaps: window.loadEvidence.heaps,
|
|
workers: window.loadEvidence.workers,
|
|
}
|
|
})
|
|
await testInfo.attach('danmuku-load-sample', { contentType: 'application/json', body: JSON.stringify(sample) })
|
|
await testInfo.attach('danmuku-load-screenshot', { contentType: 'image/png', body: await page.screenshot() })
|
|
expect(sample.mediaTime).toBeGreaterThanOrEqual(4)
|
|
expect(sample.uniqueVisible).toBeGreaterThan(0)
|
|
expect(sample.visibleNodeCount).toBeGreaterThan(0)
|
|
expect(sample.frameIntervals.length).toBeGreaterThan(1)
|
|
expect(sample.workers).toHaveLength(1)
|
|
expect(sample.workers[0].url).toMatch(/^blob:/u)
|
|
expect(sample.workers[0].posts).toBeGreaterThan(0)
|
|
expect(sample.workers[0].replies).toBeGreaterThan(0)
|
|
expect(sample.workers[0].errors).toEqual([])
|
|
await page.evaluate(() => {
|
|
window.loadArt.destroy()
|
|
window.readLoadHeap('immediately-after-destroy')
|
|
})
|
|
expect(await page.evaluate(() => window.loadArt.isDestroy)).toBe(true)
|
|
expect(await page.evaluate(() => window.loadEvidence.destroyEvents)).toBe(1)
|
|
expect(await page.evaluate(() => window.loadEvidence.workers[0].terminated)).toBe(1)
|
|
expect(external).toEqual([])
|
|
})
|
|
}
|