test(multiple-subtitles): checkpoint native seek diagnostics [PKG-MULTI-SUB-05]

This commit is contained in:
Harvey Zhao committed 2026-09-14 14:03:22 +08:00
1 parent 318de37e8a
commit a8d19ce5ef
10 files changed
+7266 -8

No files matched your search

+18
View File
@@ -283,3 +283,21 @@ multiline text/HTML, overlapping translation, selection/reset and empty interval
on5.1.2/5.1.7/5.3.0/5.4.0/candidate. The attachment records the unchanged original
host converter output alongside actual native cues and browser/artifact hashes.
It covers desktop video/track playback, not full ASS layout or physical devices.
The multiple-subtitles combination suite has opt-in source-seek diagnostics:
- `ARTPLAYER_SOURCE_SEEK_TRACE=events` records native and public events, clocks,
duration, active cues and switch settlement without replacing media properties.
- `ARTPLAYER_SOURCE_SEEK_TRACE=1` additionally forwards native `currentTime` and
`playbackRate` accessors while recording writes/stacks. The page owns these
diagnostic listeners and descriptors; do not use this helper in production.
- `ARTPLAYER_SOURCE_RESTORE_EVENT=1` selects a separate control that installs a
metadata listener before switching and, when restoration is seeking, waits for
its native seeked event as well as the original switch Promise. This tests caption
behavior after restoration; it does not repair the old public switch contract.
With no flags, the original Promise plus seeking-property path remains unchanged.
The immediate-call ordering probe also remains intact. Record the chosen flags
alongside artifacts: diagnostic overhead may change an intermittent race, and
passing the event control must not replace the original failure evidence. The
combination attachment includes the selected modes and optional native trace.
@@ -1,6 +1,9 @@
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { multipleSubtitlesCandidate, multipleSubtitlesHistorical } from '../helpers/multiple-subtitles.js'
import { expect, test } from './fixtures.js'
import { switchAfterNativeRestoration } from './source-restoration.js'
import { installSeekDiagnostics } from './source-seek-diagnostics.js'
const candidate = await multipleSubtitlesCandidate()
const historical = (await multipleSubtitlesHistorical()).filter(item => /^published-.*-main$/.test(item.name))
@@ -15,7 +18,7 @@ test.afterEach(async ({ page }, testInfo) => {
return null
const track = art.template.$track?.track
const read = cue => ({ start: cue.startTime, end: cue.endTime, text: cue.text })
return { destroyed: art.isDestroy, paused: art.video.paused, time: art.currentTime, offset: art.subtitleOffset, cues: Array.from(track?.cues || [], read), active: Array.from(track?.activeCues || [], read), displayed: art.template.$subtitle?.textContent }
return { destroyed: art.isDestroy, paused: art.video.paused, time: art.currentTime, seeking: art.video.seeking, duration: art.video.duration, readyState: art.video.readyState, offset: art.subtitleOffset, cues: Array.from(track?.cues || [], read), active: Array.from(track?.activeCues || [], read), displayed: art.template.$subtitle?.textContent, trace: window.sourceSeekTrace }
})
await testInfo.attach('multiple-combination-final-state', { contentType: 'application/json', body: JSON.stringify(state) })
})
@@ -27,7 +30,9 @@ async function prepare(page, core, implementation, browser, testInfo) {
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${implementation.code}; window.multipleFactory = module.exports.default || module.exports; })();` })
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), browser: browser.version(), scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
if (['1', 'events'].includes(process.env.ARTPLAYER_SOURCE_SEEK_TRACE))
await installSeekDiagnostics(page, process.env.ARTPLAYER_SOURCE_SEEK_TRACE === '1')
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), browser: browser.version(), traceMode: process.env.ARTPLAYER_SOURCE_SEEK_TRACE || 'off', sourceRestoration: process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1' ? 'native-event-control' : 'promise-and-seeking-property', scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
}
async function seek(page, time) {
@@ -94,7 +99,22 @@ for (const core of cores.filter(core => !['published-5.1.2', 'published-5.1.7'].
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['b']))
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
await expect(page.locator('.art-subtitle-b')).toHaveText('Translation')
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?subtitles-combination'))
if (process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1') {
await switchAfterNativeRestoration(page, '/test/pattern.mp4?subtitles-combination')
}
else {
await page.evaluate(() => {
window.recordSourceSeek?.('before-switch')
const pending = window.art.switchUrl('/test/pattern.mp4?subtitles-combination')
if (window.recordSourceSeek) {
void pending.then(
() => window.recordSourceSeek('switch-resolved'),
error => window.recordSourceSeek('switch-rejected', String(error)),
)
}
return pending
})
}
// Old core promises can settle during their native restoration seek.
// The immediate-seek path is retained separately in multiple-subtitles-switch.spec.js.
await expect.poll(() => page.evaluate(() => window.art.video.seeking)).toBe(false)
+31
View File
@@ -0,0 +1,31 @@
export async function switchAfterNativeRestoration(page, url) {
await page.evaluate((url) => {
const art = window.art
if (url === art.url)
return art.switchUrl(url)
const video = art.video
let resolveRestored
const restored = new Promise((resolve) => {
resolveRestored = resolve
})
const onSeeked = () => resolveRestored()
const onMetadata = () => {
if (video.seeking)
video.addEventListener('seeked', onSeeked, { once: true })
else
resolveRestored()
}
const cleanup = () => {
video.removeEventListener('loadedmetadata', onMetadata)
video.removeEventListener('seeked', onSeeked)
}
video.addEventListener('loadedmetadata', onMetadata, { once: true })
try {
return Promise.all([art.switchUrl(url), restored]).then(() => undefined).finally(cleanup)
}
catch (error) {
cleanup()
throw error
}
}, url)
}
+27
View File
@@ -0,0 +1,27 @@
export async function installSeekDiagnostics(page, properties = true) {
await page.evaluate((properties) => {
const art = window.art
const video = art.video
window.sourceSeekTrace = []
const state = () => ({ clock: performance.now(), time: video.currentTime, duration: video.duration, seeking: video.seeking, paused: video.paused, rate: video.playbackRate, readyState: video.readyState, source: video.currentSrc, active: Array.from(art.template.$track.track.activeCues || [], cue => cue.text) })
const record = (event, detail) => window.sourceSeekTrace.push({ event, detail, ...state() })
for (const property of properties ? ['currentTime', 'playbackRate'] : []) {
const descriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, property)
Object.defineProperty(video, property, {
configurable: true,
get() { return descriptor.get.call(this) },
set(value) {
record(`before:${property}`, { value, stack: new Error('Native media property write').stack })
descriptor.set.call(this, value)
record(`after:${property}`, { value })
},
})
}
for (const event of ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'seeking', 'seeked', 'emptied', 'pause', 'ratechange', 'timeupdate'])
video.addEventListener(event, () => record(`native:${event}`))
for (const event of ['restart', 'seek', 'fullscreen'])
art.on(event, (...values) => record(`art:${event}`, values))
window.recordSourceSeek = record
record('installed')
}, properties)
}