mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 04:01:20 -08:00
fix(multiple-subtitles): [PKG-MULTI-SUB-08] preserve entity text and semantic cue wrappers
This commit is contained in:
1 parent
4acf85ea69
commit
675b095b27
22 files changed
+744
-59
No files matched your search
@@ -1,5 +1,13 @@
|
||||
# Tests and fixture ownership
|
||||
|
||||
`test/multiple-subtitles-entities.test.js` checks named entities, literal tags,
|
||||
double encoding, nested semantic tags and selection/reset through the real plugin.
|
||||
The vendor parity suite stays unchanged: the plugin uses the parser's existing
|
||||
entity-table option. Historical malformed-tag and stray-semicolon tests continue
|
||||
to assert old defects; corrected cases have explicit rendering expectations instead
|
||||
of treating those old serialized bytes as a required contract. Browser entity
|
||||
tests compare native VTT fragments and actual HTML captions on both core variants.
|
||||
|
||||
`yarn test:library` runs actual library builds, build-input freshness checks and the
|
||||
scaffold's generated consumers. The new build modules live in `scripts/library/`;
|
||||
`yarn typecheck:library` checks them and the retained JS/MJS entrypoints. Browser
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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 literal = 'A & < > \u200E \u200F \u00A0 | <b>literal</b> | <'
|
||||
const vtt = text => `WEBVTT\n\n00:00.000 --> 00:08.000\n${text}\n`
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`Multiple subtitles ${core} core decodes entities once and preserves literal and semantic tags`, async ({ page, browser }, testInfo) => {
|
||||
const errors = []
|
||||
page.on('pageerror', error => errors.push(error.message))
|
||||
await page.route('**/test/entities-a.vtt', route => route.fulfill({ body: vtt('A & < > ‎ ‏ | <b>literal</b> | &lt;'), contentType: 'text/vtt' }))
|
||||
await page.route('**/test/entities-b.vtt', route => route.fulfill({ body: vtt('<b>bold & safe</b> tail <i>'), contentType: 'text/vtt' }))
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.evaluate(async () => {
|
||||
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ name: 'a', url: '/test/entities-a.vtt' }, { name: 'b', url: '/test/entities-b.vtt' }] }))
|
||||
})
|
||||
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.poll(() => page.locator('.art-subtitle-a').textContent()).toBe(literal)
|
||||
await expect(page.locator('.art-subtitle-a b')).toHaveCount(0)
|
||||
await expect(page.locator('.art-subtitle-b b')).toHaveText('bold & safe')
|
||||
await expect(page.locator('.art-subtitle-b i')).toHaveCount(0)
|
||||
expect(await page.locator('.art-subtitle-b').allTextContents()).toEqual(['bold & safe', ' tail <i>'])
|
||||
const native = await page.evaluate(() => window.art.subtitle.cues.map(cue => cue.getCueAsHTML().textContent))
|
||||
expect(native).toEqual([literal, 'bold & safe tail <i>'])
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks(['a']))
|
||||
await expect(page.locator('.art-subtitle-b')).toHaveCount(0)
|
||||
await expect.poll(() => page.locator('.art-subtitle-a').textContent()).toBe(literal)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect(page.locator('.art-subtitle-b b')).toHaveText('bold & safe')
|
||||
await testInfo.attach('entities.json', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(implementation.code), native, literal, scope: 'Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only' }) })
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Text entity regressions use real built plugin serialization.
|
||||
import test from 'node:test'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const implementation = await multipleSubtitlesCandidate()
|
||||
for (const [input, expected] of [
|
||||
['& < > ‎ ‏ ', '& < > \u200E \u200F \u00A0'],
|
||||
['Literal <00:02.000> and <b>text</b>', 'Literal <00:02.000> and <b>text</b>'],
|
||||
['&lt; &amp; &gt;', '< & >'],
|
||||
['One &; two <;', 'One &; two <;'],
|
||||
['<b>bold & safe</b> tail <i>', 'bold & safe tail <i>'],
|
||||
['<c.red>color</c> <i>italic</i>', 'color italic'],
|
||||
['<v Speaker>voice</v>', 'voice'],
|
||||
['<ruby>text<rt>note</rt></ruby>', 'textnote'],
|
||||
['&unknown; &litude; &', '&unknown; &litude; &'],
|
||||
['A A', 'A A'],
|
||||
]) {
|
||||
test(`Multiple subtitles entity text remains literal after selection/reset: ${input}`, async () => {
|
||||
const env = multipleSubtitlesEnvironment(implementation, { responses: { 'a.vtt': subtitleVtt(input), 'b.vtt': subtitleVtt('Other') } })
|
||||
const result = await env.factory({ subtitles: [{ name: 'a', url: 'a.vtt' }, { name: 'b', url: 'b.vtt' }] })(env.art)
|
||||
for (const names of [['a'], ['b', 'a'], ['a', 'a']]) {
|
||||
result.tracks(names)
|
||||
const vtt = env.utils.unescape(await env.latestText())
|
||||
const text = vtt.split(/\n\d[^\n]*-->[^\n]*\n/).slice(1).join('')
|
||||
const { document } = parseHTML(`<html><body>${text}</body></html>`)
|
||||
assert.equal(document.body.textContent.trim(), names.map(name => name === 'a' ? expected : 'Other').join('\n\n').trim())
|
||||
if (input.includes('<i>'))
|
||||
assert.equal(document.querySelectorAll('i').length, 0)
|
||||
if (input.startsWith('<b>'))
|
||||
assert.equal(document.querySelector('b').textContent, 'bold & safe')
|
||||
}
|
||||
result.reset()
|
||||
assert((env.utils.unescape(await env.latestText())).includes('Other'))
|
||||
env.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('Multiple subtitles raw ampersand and malformed angle text retain historical visible output', async () => {
|
||||
const historical = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main')
|
||||
const output = []
|
||||
for (const code of [historical, implementation]) {
|
||||
const env = multipleSubtitlesEnvironment(code, { responses: { 'a.vtt': subtitleVtt('A & B < C > D') } })
|
||||
await env.factory({ subtitles: [{ name: 'a', url: 'a.vtt' }] })(env.art)
|
||||
const { document } = parseHTML(`<html><body>${env.utils.unescape(await env.latestText())}</body></html>`)
|
||||
output.push(document.body.textContent)
|
||||
env.emit('destroy')
|
||||
}
|
||||
assert.equal(output[1], output[0])
|
||||
})
|
||||
@@ -269,4 +269,10 @@ for (const implementation of implementations) {
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert(env.utils.unescape(await env.latestText()).includes('Literal <;00:02.000>; example &;'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: semantic tag wrappers become malformed attributes`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': subtitleVtt('<b>bold</b>') } })
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert(env.utils.unescape(await env.latestText()).includes('<b <div class="art-subtitle-a">undefined</div>>bold</b>'))
|
||||
})
|
||||
}
|
||||
@@ -31,13 +31,9 @@ test('Multiple subtitles merge: repeated selection serializes frozen trees witho
|
||||
assert.equal(JSON.stringify(trees), original)
|
||||
})
|
||||
|
||||
test('Multiple subtitles merge: actual released serializer output is preserved for text, markup and timing cases', async () => {
|
||||
test('Multiple subtitles merge: released serializer output is preserved outside corrected text escaping and tag wrappers', async () => {
|
||||
const samples = [
|
||||
subtitleVtt('A & B < C > D'),
|
||||
subtitleVtt('<b>bold</b> tail'),
|
||||
subtitleVtt('<c.red>color</c> <i>italic</i>'),
|
||||
subtitleVtt('<v Speaker>voice</v>'),
|
||||
subtitleVtt('<ruby>text<rt>note</rt></ruby>'),
|
||||
subtitleVtt('Plain text'),
|
||||
subtitleVtt('line one\nline two'),
|
||||
'WEBVTT\n\n00:02.000 --> 00:05.000\nLater\n\n00:00.000 --> 00:03.000\nEarlier\n',
|
||||
subtitleVtt('best effort').replace('WEBVTT', 'BAD HEADER'),
|
||||
|
||||
Reference in new issue
Block a user