fix(multiple-subtitles): render overlapping cues on legacy hosts [PKG-MULTI-SUB-10]

This commit is contained in:
Harvey Zhao committed 2026-09-14 13:26:42 +08:00
1 parent 31f9024bff
commit 8ae2762a23
24 files changed
+3465 -56

No files matched your search

+11
View File
@@ -266,3 +266,14 @@ syntax failures remain in the historical parser tests. Mobile cases use an Andro
UA and synthetic DOM touches through the real core handlers, not trusted hardware
input. Fullscreen mouse placement first waits for the progress control to be stable;
the earlier hidden-preview screenshot timeout remains in the checkpoint evidence.
`multiple-subtitles-legacy.spec.js` uses real 5.1.2/5.1.7 cores and the candidate
plugin to check three independently timed captions, VTT/SRT, semantic HTML/CSS,
the original update method and native cue identities, scalar event payloads,
repeated updates, selection/reset and empty intervals. It uses ASS for the third
track on5.1.7; the5.1.2 published converter's malformed VTT remains a separate
task11 failure and is not covered by substituting a VTT fixture. Use
`ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT` for actual main/legacy package bytes.
The combination suite retains explicit published1.1/1.2 overlap-loss observations
while requiring candidate captions to contain both languages. A passing historical
observation is not a compatibility pass.
@@ -172,18 +172,19 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
await window.art.plugins.add(window.multipleFactory({ subtitles: [{ name: 'a', url: '/test/combination.vtt' }, { name: 'b', url: '/test/combination.srt' }] }))
})
const indexMerge = implementation.version === '1.0.0'
const displaysBoth = indexMerge || implementation.name === 'candidate'
await expect.poll(() => page.evaluate(() => window.art.template.$track.track.cues?.length)).toBe(indexMerge ? 1 : 2)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await seek(page, 1.5)
await expect(page.locator('.art-subtitle-a')).toHaveText('First')
// This is a deficiency probe, not successful simultaneous-caption acceptance.
await expect(page.locator('.art-subtitle-b')).toHaveCount(indexMerge ? 1 : 0)
// Historical 1.1/1.2 failures remain observations; candidate must display both.
await expect(page.locator('.art-subtitle-b')).toHaveCount(displaysBoth ? 1 : 0)
const state = await page.evaluate(() => ({ native: Array.from(window.art.template.$track.track.activeCues, cue => cue.getCueAsHTML().textContent), displayed: window.art.template.$subtitle.textContent }))
expect(state.native).toEqual(indexMerge ? ['FirstTranslation'] : ['First', 'Translation'])
expect(state.displayed).toBe(indexMerge ? 'FirstTranslation' : 'First')
await testInfo.attach('single-active-cue-boundary', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, state, simultaneousCaptionsAccepted: indexMerge, unresolvedCandidateGap: !indexMerge, explanation: '1.0.0 combines by array index; 1.1.0 onward retains separately timed cues. Old core displays only its first active cue. Candidate gap remains open, even when this observation test passes.' }) })
expect(state.displayed).toBe(displaysBoth ? 'FirstTranslation' : 'First')
await testInfo.attach('single-active-cue-boundary', { contentType: 'application/json', body: JSON.stringify({ core, implementation: implementation.name, state, simultaneousCaptionsAccepted: displaysBoth, historicalCaptionLoss: !displaysBoth, explanation: '1.0.0 combines by array index; 1.1/1.2 retain separate cues but the old core displays only its first. Candidate adapts the old view while retaining separate native timings.' }) })
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['b']))
await expect(page.locator('.art-subtitle-b')).toHaveText('Translation')
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
@@ -0,0 +1,76 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
import { expect, test } from './fixtures.js'
const candidate = await multipleSubtitlesCandidate()
for (const core of ['published-5.1.2', 'published-5.1.7']) {
test(`Multiple subtitles legacy host ${core}: independently timed captions overlap and clear`, async ({ page, browser }, testInfo) => {
await page.route('**/test/legacy-a.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:01.000 --> 00:03.000\n<b>A &amp; safe</b>\n' }))
await page.route('**/test/legacy-b.srt', route => route.fulfill({ contentType: 'text/plain', body: '1\n00:00:02,000 --> 00:00:04,000\nB\n' }))
await page.route('**/test/legacy-c.ass', route => route.fulfill({ contentType: 'text/plain', body: 'Dialogue: 0,0:00:02.25,0:00:02.75,Default,,0,0,0,,C' }))
await page.route('**/test/legacy-c.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:02.250 --> 00:02.750\nC\n' }))
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: candidate.code })
await page.addStyleTag({ content: '.art-subtitle-a { color: rgb(255, 0, 0); } .art-subtitle-b { color: rgb(0, 0, 255); }' })
await page.evaluate(() => {
window.createPlayer('/test/pattern.mp4')
window.originalSubtitleUpdate = window.art.subtitle.update
window.legacyPayloads = []
window.art.on('subtitleUpdate', text => window.legacyPayloads.push(text))
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
const thirdFormat = core === 'published-5.1.2' ? 'vtt' : 'ass'
await page.evaluate(async (thirdFormat) => {
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'a', url: '/test/legacy-a.vtt' }, { name: 'b', url: '/test/legacy-b.srt' }, { name: 'c', url: `/test/legacy-c.${thirdFormat}` }] }))
}, thirdFormat)
await expect.poll(() => page.evaluate(() => window.art.template.$track.track.cues?.length)).toBe(3)
await page.evaluate(() => {
window.cueIdentities = Array.from(window.art.template.$track.track.cues)
})
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
async function seek(time) {
await page.evaluate(time => new Promise((resolve) => {
window.art.once('video:seeked', resolve)
window.art.seek = time
}), time)
}
await seek(1.5)
await expect(page.locator('.art-subtitle-a b')).toHaveText('A & safe')
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
await seek(2.5)
await expect(page.locator('.art-subtitle-a b')).toHaveText('A & safe')
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
await expect(page.locator('.art-subtitle-a')).toHaveCSS('color', 'rgb(255, 0, 0)')
await expect(page.locator('.art-subtitle-b')).toHaveCSS('color', 'rgb(0, 0, 255)')
const state = await page.evaluate(() => {
const art = window.art
art.subtitle.update()
art.subtitle.update()
const cues = Array.from(art.template.$track.track.cues)
return { sameUpdate: art.subtitle.update === window.originalSubtitleUpdate, sameCues: cues.every((cue, index) => cue === window.cueIdentities[index]), times: cues.map(cue => [cue.startTime, cue.endTime]), legacyPayload: window.legacyPayloads.at(-1), firstText: art.template.$track.track.activeCues[0].text, counts: ['a', 'b', 'c'].map(name => art.template.$subtitle.querySelectorAll(`.art-subtitle-${name}`).length) }
})
expect(state.sameUpdate).toBe(true)
expect(state.sameCues).toBe(true)
expect(state.times).toEqual([[1, 3], [2, 4], [2.25, 2.75]])
expect(state.legacyPayload).toBe(state.firstText)
expect(state.counts).toEqual([1, 1, 1])
await seek(3.5)
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
await expect(page.locator('.art-subtitle-c')).toHaveCount(0)
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(page.locator('.art-subtitle-b')).toHaveText('B')
await seek(2.5)
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
await seek(4.5)
await expect(page.locator('.art-subtitle-a, .art-subtitle-b, .art-subtitle-c')).toHaveCount(0)
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), thirdFormat, state, scope: 'Native VTT/SRT overlaps (ASS additionally on 5.1.7), semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals', limitation: '5.1.2 published ASS converter loses required VTT line breaks; tracked separately in PKG-MULTI-SUB-11.' }) })
await page.evaluate(() => window.art.destroy())
})
}