test(audio): [PKG-AUDIO-05] distinguish prefix startup from native starvation

This commit is contained in:
Harvey Zhao committed 2026-09-15 13:12:05 +08:00
1 parent bf56d09178
commit 3eb5500525
9 files changed
+4151 -11

No files matched your search

+11
View File
@@ -230,6 +230,17 @@ playback after release; inspect its progressed/waiting flags before claiming buf
These capability observation timeouts do not relax the integration assertions.
Archive the report and results (including traces) before starting another browser suite.
For byte-prefix diagnostics, set `ARTPLAYER_MEDIA_GATE_VIDEO_LIMIT` and/or
`ARTPLAYER_MEDIA_GATE_AUDIO_LIMIT` to an integer byte count strictly between zero
and the corresponding file size. Unset them for the unchanged 96 KiB video / 32 KiB
audio defaults. These variables only affect the native diagnostic, never the audio
plugin integration test. `native-gate-before-release` retains the media state and
an immutable request snapshot before releasing the tail; `native-gate-capability`
also survives failures after that observation and records any successful recovery.
Check `progressed` and `waiting` independently. Larger prefixes can start Windows
WebKit playback without yielding a trusted waiting event; this does not prove the
integration buffering contract. See the audio prefix checkpoint in refactor.
Danmuku input, scheduler, resources and heatmap-density specs accept
`ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the
current JS/TS entry. Run each format sequentially and wait for the command's final
+12 -7
View File
@@ -1,4 +1,5 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { createMediaGate } from '../helpers/media-gate.js'
import { expect, test } from './fixtures.js'
@@ -8,8 +9,10 @@ for (const owner of ['video', 'audio']) {
test(`native ${owner} ${completeRanges ? 'complete ranges' : 'streamed response'} capability without ArtPlayer instances`, async ({ page }, testInfo) => {
const file = owner === 'video' ? 'pattern.mp4' : 'audio-tone.m4a'
const bytes = fs.readFileSync(new URL(`./media/${file}`, import.meta.url))
const limit = owner === 'video' ? 96 * 1024 : 32 * 1024
const configuredLimit = process.env[`ARTPLAYER_MEDIA_GATE_${owner.toUpperCase()}_LIMIT`]
const limit = configuredLimit === undefined ? (owner === 'video' ? 96 * 1024 : 32 * 1024) : Number(configuredLimit)
const gate = await createMediaGate(bytes, owner === 'video' ? 'video/mp4' : 'audio/mp4', limit, completeRanges)
const observation = { file, sha256: hash(bytes), bytes: bytes.length, limit, completeRanges, configuredLimit: configuredLimit ?? null, progressed: false, waiting: false }
try {
await page.goto('/test/player.html')
await page.evaluate(({ owner, url }) => {
@@ -25,29 +28,31 @@ for (const owner of ['video', 'audio']) {
}, { owner, url: gate.url })
await expect.poll(() => page.evaluate(() => window.nativeMedia.readyState >= 2 || Boolean(window.nativeMedia.error))).toBe(true)
await page.locator('#play').click()
let progressed = false
let waiting = false
try {
await page.waitForFunction(() => window.nativeMedia.currentTime > 0.3, null, { timeout: 3000 })
progressed = true
observation.progressed = true
await page.waitForFunction(() => window.nativeEvents.some(event => event.event === 'waiting' && event.trusted && event.time > 0.3 && event.readyState < 3), null, { timeout: 7000 })
waiting = true
observation.waiting = true
}
catch (error) {
if (error.name !== 'TimeoutError')
throw error
}
const before = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused, error: window.nativeMedia.error?.code || null, instances: window.Artplayer.instances.length, events: window.nativeEvents }))
const before = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, duration: window.nativeMedia.duration, ended: window.nativeMedia.ended, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused, error: window.nativeMedia.error?.code || null, instances: window.Artplayer.instances.length, events: window.nativeEvents, buffered: Array.from({ length: window.nativeMedia.buffered.length }, (_, index) => [window.nativeMedia.buffered.start(index), window.nativeMedia.buffered.end(index)]) }))
observation.before = before
observation.requestsBeforeRelease = structuredClone(gate.requests)
await testInfo.attach('native-gate-before-release', { contentType: 'application/json', body: JSON.stringify(observation) })
expect(before.instances).toBe(0)
expect(gate.blocked).toBeGreaterThan(0)
gate.release()
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.nativeMedia.currentTime)).toBeGreaterThan(before.time + 0.3)
expect(await page.evaluate(() => window.nativeMedia.error)).toBeNull()
await testInfo.attach('native-gate-capability', { contentType: 'application/json', body: JSON.stringify({ file, sha256: hash(bytes), limit, completeRanges, progressed, waiting, before, requests: gate.requests, after: await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused })) }) })
observation.after = await page.evaluate(() => ({ time: window.nativeMedia.currentTime, readyState: window.nativeMedia.readyState, paused: window.nativeMedia.paused }))
}
finally {
try {
await testInfo.attach('native-gate-capability', { contentType: 'application/json', body: JSON.stringify({ ...observation, requests: gate.requests }) })
await page.evaluate(() => {
window.nativeMedia?.pause()
window.nativeMedia?.removeAttribute('src')