import assert from 'node:assert/strict' import { Buffer } from 'node:buffer' // eslint-disable-next-line test/no-import-node-test -- Merge and decoder regressions use the repository Node runner. import test from 'node:test' import { loadModules } from './helpers/load.js' import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js' const { parseTracks, serializeTracks, ...coreUtils } = await loadModules({ parseTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'parseTracks' }, serializeTracks: { file: 'packages/artplayer-plugin-multiple-subtitles/src/merge', name: 'serializeTracks' }, getExt: { file: 'packages/artplayer/src/utils/file', name: 'getExt' }, srtToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'srtToVtt' }, assToVtt: { file: 'packages/artplayer/src/utils/subtitle', name: 'assToVtt' }, }) const candidate = await multipleSubtitlesCandidate() const historical = (await multipleSubtitlesHistorical()).find(item => item.name === 'published-1.2.0-main') function freeze(value) { if (value && typeof value === 'object') { Object.freeze(value) for (const child of Object.values(value)) freeze(child) } return value } test('Multiple subtitles merge: repeated selection serializes frozen trees without accumulating wrappers', () => { const trees = freeze(parseTracks([subtitleVtt('A'), subtitleVtt('B')], [{ name: 'a' }, { name: 'b' }])) const original = JSON.stringify(trees) const first = serializeTracks(trees) serializeTracks([trees[1], trees[0], trees[1]]) assert.equal(serializeTracks(trees), first) assert.equal(JSON.stringify(trees), original) }) test('Multiple subtitles merge: released serializer output is preserved outside corrected text escaping and tag wrappers', async () => { const samples = [ 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'), 'WEBVTT\n\n', ] for (const body of samples) { const environments = [historical, candidate].map(implementation => multipleSubtitlesEnvironment(implementation, { responses: { 'a.vtt': body, 'b.vtt': subtitleVtt('second') } })) const results = await Promise.all(environments.map(env => env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art))) for (const selection of [null, ['b', 'a', 'b'], [], ['a']]) { if (selection) results.forEach(result => result.tracks(selection)) assert.equal(await environments[1].latestText(), await environments[0].latestText(), body) } results.forEach(result => result.reset()) assert.equal(await environments[1].latestText(), await environments[0].latestText(), body) environments[1].emit('destroy') } }) test('Multiple subtitles request: real core SRT/ASS conversion and encoded text retain released output', async () => { const responses = { 'a.SRT?track=1': '1\n00:00:00,000 --> 00:00:02,000\nSRT text\n', 'b.ass': 'Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,,ASS text', 'c.vtt': Buffer.from(subtitleVtt('café'), 'latin1'), 'd.vtt': Buffer.from(subtitleVtt('日本語'), 'utf16le'), } const options = { subtitles: [{ url: 'a.SRT?track=1', name: 'srt' }, { url: 'b.ass', name: 'ass' }, { url: 'c.vtt', name: 'latin', encoding: 'windows-1252' }, { url: 'd.vtt', name: 'utf16', encoding: 'utf-16le' }] } const old = multipleSubtitlesEnvironment(historical, { responses, coreUtils }) const current = multipleSubtitlesEnvironment(candidate, { responses, coreUtils }) await old.factory(options)(old.art) await current.factory(options)(current.art) const text = await current.latestText() assert.equal(text, await old.latestText()) for (const value of ['SRT text', 'ASS text', 'café', '日本語']) assert(text.includes(value)) current.emit('destroy') }) for (const [text, timestamps] of [ ['Before <00:02.000> after', ['<00:02.000>']], ['<00:01.250> one <00:03.500> two', ['<00:01.250>', '<00:03.500>']], ['end <00:07.000>', ['<00:07.000>']], ['甲<00:02.000>乙', ['<00:02.000>']], ['bold <00:02.000> after', ['<00:02.000>']], ]) { test(`Multiple subtitles timestamps: valid nodes survive selection and reset (${text})`, async () => { const input = `WEBVTT\n\n00:00.000 --> 00:08.000\n${text}\n` const env = multipleSubtitlesEnvironment(candidate, { responses: { 'a.vtt': input, 'b.vtt': subtitleVtt('Other') } }) const result = await env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art) result.tracks(['a']) const selected = await env.latestText() for (const timestamp of timestamps) assert(selected.includes(timestamp)) assert(!selected.includes('NaN')) const reparsed = parseTracks([selected], [{ name: 'reparsed' }]) assert.deepEqual(reparsed[0].errors, []) const numbers = [] function collect(nodes) { for (const node of nodes) { if (node.type === 'timestamp') numbers.push(node.value) if (node.children) collect(node.children) } } collect(reparsed[0].cues[0].tree.children) assert.equal(numbers.length, timestamps.length) result.tracks(['b']) assert(!(await env.latestText()).includes(timestamps[0])) result.reset() for (const timestamp of timestamps) assert((await env.latestText()).includes(timestamp)) env.emit('destroy') }) } test('Multiple subtitles timestamps: frozen numeric nodes stay unchanged across duplicate selection', () => { const trees = freeze(parseTracks(['WEBVTT\n\n00:00.000 --> 00:08.000\nBefore <00:02.000> after\n'], [{ name: 'a' }])) const original = JSON.stringify(trees) const output = serializeTracks([trees[0], trees[0]]) assert.equal((output.match(/<00:02\.000>/g) || []).length, 2) assert.equal((output.match(//g) || []).length, 2) assert.equal(JSON.stringify(trees), original) })