mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
215 lines
12 KiB
JavaScript
215 lines
12 KiB
JavaScript
import { hash } from '../../refactor/scripts/releases.mjs'
|
|
import { multipleSubtitlesCandidate } from '../helpers/multiple-subtitles.js'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const implementation = await multipleSubtitlesCandidate()
|
|
const vtt = 'WEBVTT\n\n00:00.000 --> 00:08.000\nCached caption\n'
|
|
const srt = text => `1\n00:00:00,000 --> 00:00:08,000\n${text}\n`
|
|
|
|
async function prepare(page, core) {
|
|
await page.goto(`/test/player.html?core=${core}`)
|
|
await page.addScriptTag({ content: implementation.code })
|
|
await page.evaluate(() => {
|
|
window.createPlayer('/test/pattern.mp4')
|
|
window.multipleProbe = { initialized: [], revoked: [], signal: null, errors: [], requests: [] }
|
|
const fetch = window.fetch.bind(window)
|
|
window.multipleNativeFetch = fetch
|
|
window.fetch = (url, options) => {
|
|
if (typeof url === 'string' && url.startsWith('/test/multiple-'))
|
|
window.multipleProbe.requests.push(url)
|
|
if (url === '/test/multiple-held.vtt')
|
|
window.multipleProbe.signal = options?.signal
|
|
return fetch(url, options)
|
|
}
|
|
const revoke = URL.revokeObjectURL.bind(URL)
|
|
URL.revokeObjectURL = (url) => {
|
|
window.multipleProbe.revoked.push(url)
|
|
revoke(url)
|
|
}
|
|
const init = window.art.subtitle.init.bind(window.art.subtitle)
|
|
window.art.subtitle.init = (option) => {
|
|
window.multipleProbe.initialized.push({ url: option.url, destroyed: window.art.isDestroy })
|
|
const result = init(option)
|
|
Promise.resolve(result).catch(error => window.multipleProbe.errors.push({ name: error.name, message: error.message }))
|
|
return result
|
|
}
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
}
|
|
|
|
async function evidence(page, testInfo, core, observation) {
|
|
const probe = await page.evaluate(() => ({ ...window.multipleProbe, signal: window.multipleProbe.signal ? { aborted: window.multipleProbe.signal.aborted } : null }))
|
|
expect(probe.errors).toEqual([])
|
|
await testInfo.attach('multiple-subtitles-lifecycle', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance }, observation, probe }), contentType: 'application/json' })
|
|
}
|
|
|
|
for (const core of ['published', 'candidate']) {
|
|
test(`Multiple subtitles candidate ${core} core: SRT display and selectors survive the resource refactor`, async ({ page }, testInfo) => {
|
|
await page.route('**/test/multiple-en.srt', route => route.fulfill({ body: srt('English'), contentType: 'text/plain' }))
|
|
await page.route('**/test/multiple-jp.srt', route => route.fulfill({ body: srt('日本語'), contentType: 'text/plain' }))
|
|
await prepare(page, core)
|
|
await page.evaluate(async () => {
|
|
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'en', url: '/test/multiple-en.srt' }, { name: 'jp', url: '/test/multiple-jp.srt' }] }))
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)
|
|
await page.locator('#play').click()
|
|
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
|
await page.locator('#pause').click()
|
|
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
|
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['jp']))
|
|
await expect(page.locator('.art-subtitle-en')).toHaveCount(0)
|
|
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
|
await expect(page.locator('.art-subtitle-jp')).toHaveCount(0)
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
|
await expect(page.locator('.art-subtitle-en')).toHaveText('English')
|
|
await expect(page.locator('.art-subtitle-jp')).toHaveText('日本語')
|
|
const released = await page.evaluate(() => {
|
|
window.art.destroy(false)
|
|
return window.multipleProbe.initialized.every(item => window.multipleProbe.revoked.includes(item.url))
|
|
})
|
|
expect(released).toBe(true)
|
|
await evidence(page, testInfo, core, { nativeSrtDisplay: true, released })
|
|
})
|
|
|
|
test(`Multiple subtitles candidate ${core} core: destroy settles a pending native request before it responds`, async ({ page }, testInfo) => {
|
|
let requested
|
|
let release
|
|
const started = new Promise((resolve) => {
|
|
requested = resolve
|
|
})
|
|
const held = new Promise((resolve) => {
|
|
release = resolve
|
|
})
|
|
await page.route('**/test/multiple-held.vtt', async (route) => {
|
|
requested()
|
|
await held
|
|
await route.fulfill({ body: vtt, contentType: 'text/vtt' })
|
|
})
|
|
try {
|
|
await prepare(page, core)
|
|
await page.evaluate(() => {
|
|
window.multiplePending = window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-held.vtt', name: 'held' }] })(window.art)
|
|
})
|
|
await started
|
|
const state = await page.evaluate(async () => {
|
|
window.art.destroy(false)
|
|
const result = await window.multiplePending
|
|
return { name: result.name, voidReset: result.reset() === undefined, initialized: window.multipleProbe.initialized.length, aborted: window.multipleProbe.signal.aborted }
|
|
})
|
|
expect(state).toEqual({ name: 'multipleSubtitles', voidReset: true, initialized: 0, aborted: true })
|
|
release()
|
|
await evidence(page, testInfo, core, state)
|
|
}
|
|
finally { release() }
|
|
})
|
|
|
|
test(`Multiple subtitles candidate ${core} core: destroy revokes the Blob and retained reset stays inert`, async ({ page }, testInfo) => {
|
|
await page.route('**/test/multiple-ready.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' }))
|
|
await prepare(page, core)
|
|
await page.evaluate(async () => {
|
|
window.multipleResult = await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-ready.vtt', name: 'ready' }] })(window.art)
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
|
const state = await page.evaluate(async () => {
|
|
const url = window.multipleProbe.initialized[0].url
|
|
window.art.destroy(false)
|
|
const voidReset = window.multipleResult.reset() === undefined
|
|
let inaccessible = false
|
|
try {
|
|
await window.multipleNativeFetch(url)
|
|
}
|
|
catch (error) {
|
|
inaccessible = error.name === 'TypeError'
|
|
}
|
|
return { voidReset, inaccessible, initialized: window.multipleProbe.initialized.length, revoked: window.multipleProbe.revoked.includes(url) }
|
|
})
|
|
expect(state).toEqual({ voidReset: true, inaccessible: true, initialized: 1, revoked: true })
|
|
await evidence(page, testInfo, core, state)
|
|
})
|
|
|
|
test(`Multiple subtitles candidate ${core} core: native HTTP error rejects without installing subtitles`, async ({ page }, testInfo) => {
|
|
await page.route('**/test/multiple-error.vtt', route => route.fulfill({ status: 404, body: vtt, contentType: 'text/vtt' }))
|
|
await prepare(page, core)
|
|
const state = await page.evaluate(async () => {
|
|
let message
|
|
try {
|
|
await window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-error.vtt', name: 'error' }] })(window.art)
|
|
}
|
|
catch (error) {
|
|
message = error.message
|
|
}
|
|
window.art.destroy(false)
|
|
return { message, initialized: window.multipleProbe.initialized.length }
|
|
})
|
|
expect(state).toEqual({ message: 'Failed to fetch multiple subtitles: HTTP 404', initialized: 0 })
|
|
await evidence(page, testInfo, core, state)
|
|
})
|
|
|
|
test(`Multiple subtitles candidate ${core} core: video source switching retains cached subtitle selection`, async ({ page }, testInfo) => {
|
|
await page.route('**/test/multiple-restart.vtt', route => route.fulfill({ body: vtt, contentType: 'text/vtt' }))
|
|
await prepare(page, core)
|
|
await page.evaluate(async () => {
|
|
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-restart.vtt', name: 'restart' }] }))
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
|
await page.evaluate(() => window.art.switchUrl('/test/pattern.mp4?multiple-restart=1'))
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
|
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
|
await page.locator('#play').click()
|
|
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
|
await page.locator('#pause').click()
|
|
await expect(page.locator('.art-subtitle-restart')).toHaveText('Cached caption')
|
|
const requests = await page.evaluate(() => window.multipleProbe.requests)
|
|
expect(requests).toEqual(['/test/multiple-restart.vtt'])
|
|
await page.evaluate(() => window.art.destroy(false))
|
|
await evidence(page, testInfo, core, { requests, nativeSourceSwitch: true })
|
|
})
|
|
|
|
test(`Multiple subtitles candidate ${core} core: inline timestamps remain native instructions without visible time tags`, async ({ page }, testInfo) => {
|
|
const timed = 'WEBVTT\n\n00:00.000 --> 00:08.000\n<00:01.250>Before <00:02.000> after<00:03.500>\n'
|
|
await page.route('**/test/multiple-timed.vtt', route => route.fulfill({ body: timed, contentType: 'text/vtt' }))
|
|
await prepare(page, core)
|
|
await page.addStyleTag({ content: '.art-subtitle-timed { color: rgb(255, 0, 0); }' })
|
|
await page.evaluate(async () => {
|
|
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-timed.vtt', name: 'timed' }] }))
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
|
const native = await page.evaluate(() => {
|
|
const cue = window.art.subtitle.cues[0]
|
|
const fragment = cue.getCueAsHTML()
|
|
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_PROCESSING_INSTRUCTION)
|
|
const timestamps = []
|
|
while (walker.nextNode()) timestamps.push(walker.currentNode.nodeValue)
|
|
return { text: cue.text, timestamps }
|
|
})
|
|
expect(native.text).toContain('<00:02.000>')
|
|
expect(native.text).not.toContain('NaN')
|
|
expect(native.timestamps).toHaveLength(3)
|
|
await page.locator('#play').click()
|
|
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
|
await page.locator('#pause').click()
|
|
await expect(page.locator('.art-subtitle-timed').first()).toHaveCSS('color', 'rgb(255, 0, 0)')
|
|
const timedLine = page.locator('.art-subtitle-line').filter({ has: page.locator('.art-subtitle-timed') })
|
|
const visible = await timedLine.textContent()
|
|
expect(visible).toContain('Before')
|
|
expect(visible).toContain('after')
|
|
expect(visible).not.toMatch(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
|
|
// Public cue edits exercise literal display without relying on the vendor's entity decoder.
|
|
await page.evaluate(() => {
|
|
window.art.subtitle.cues[0].text += '<div class="art-subtitle-literal">Literal <00:02.000> example</div>'
|
|
window.art.subtitle.update()
|
|
})
|
|
await expect(page.locator('.art-subtitle-literal')).toHaveText('Literal <00:02.000> example')
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
|
await expect(page.locator('.art-subtitle-timed')).toHaveCount(0)
|
|
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
|
await expect(timedLine).toContainText('Before')
|
|
await expect(timedLine).not.toContainText(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
|
|
await expect(page.locator('.art-subtitle-literal')).toHaveCount(0)
|
|
await page.evaluate(() => window.art.destroy(false))
|
|
await evidence(page, testInfo, core, { native, visible })
|
|
})
|
|
}
|