mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 05:41:20 -08:00
test(multiple-subtitles): [PKG-MULTI-SUB-01] freeze contracts and verify parser provenance
This commit is contained in:
1 parent
0c5bb2bd7a
commit
5a0d94cb89
33 files changed
+2375
-22
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import { verifyMultipleSubtitlesContract } from '../../refactor/scripts/multiple-subtitles-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from './load.js'
|
||||
|
||||
export const subtitleVtt = text => `WEBVTT\n\n00:00.000 --> 00:02.000\n${text}\n`
|
||||
|
||||
export async function multipleSubtitlesCandidate() {
|
||||
return { name: 'candidate', version: 'candidate', code: process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-multiple-subtitles', 'umd') }
|
||||
}
|
||||
|
||||
export async function multipleSubtitlesHistorical() {
|
||||
const { baseline, archives, sources } = await verifyMultipleSubtitlesContract()
|
||||
const name = 'artplayer-plugin-multiple-subtitles'
|
||||
const implementations = []
|
||||
for (const release of [baseline.release, ...baseline.previous]) {
|
||||
for (const field of ['main', 'legacy'])
|
||||
implementations.push({ name: `published-${release.version}-${field}`, version: release.version, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
|
||||
}
|
||||
const source = sources.get(`packages/${name}/src/parser.js`)
|
||||
+ sources.get(`packages/${name}/src/index.js`).replace(/^import .* from ['"].\/parser['"];?\s*$/m, '')
|
||||
implementations.push({ name: 'frozen-workspace-source', version: 'workspace', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code })
|
||||
for (const suffix of ['js', 'legacy.js'])
|
||||
implementations.push({ name: `frozen-workspace-${suffix}`, version: 'workspace', code: sources.get(`packages/${name}/dist/${name}.${suffix}`) })
|
||||
return implementations
|
||||
}
|
||||
|
||||
export function multipleSubtitlesEnvironment(implementation, { script = false, responses = {} } = {}) {
|
||||
const requests = []
|
||||
const blobs = new Map()
|
||||
const revoked = []
|
||||
const initialized = []
|
||||
const converted = []
|
||||
const module = { exports: {} }
|
||||
const utils = {
|
||||
getExt: url => url.split('.').pop(),
|
||||
unescape: text => text.replaceAll('<', '<').replaceAll('>', '>').replaceAll('&', '&'),
|
||||
srtToVtt(text) {
|
||||
converted.push(['srt', text])
|
||||
return subtitleVtt('SRT')
|
||||
},
|
||||
assToVtt(text) {
|
||||
converted.push(['ass', text])
|
||||
return subtitleVtt('ASS')
|
||||
},
|
||||
}
|
||||
const art = {
|
||||
constructor: { utils },
|
||||
option: { subtitle: { escape: true, style: { color: 'red' } } },
|
||||
subtitle: { init(option) { initialized.push(option) } },
|
||||
}
|
||||
const context = {
|
||||
Blob,
|
||||
TextDecoder,
|
||||
console: { log() {}, warn() {}, error() {} },
|
||||
URL: {
|
||||
createObjectURL(blob) {
|
||||
const url = `blob:subtitle-${blobs.size + 1}`
|
||||
blobs.set(url, blob)
|
||||
return url
|
||||
},
|
||||
revokeObjectURL(url) { revoked.push(url) },
|
||||
},
|
||||
fetch: async (url) => {
|
||||
requests.push(url)
|
||||
const bytes = new TextEncoder().encode(responses[url] ?? subtitleVtt(url))
|
||||
return { ok: true, arrayBuffer: async () => bytes.buffer }
|
||||
},
|
||||
}
|
||||
context.window = context
|
||||
if (!script) {
|
||||
context.module = module
|
||||
context.exports = module.exports
|
||||
}
|
||||
vm.runInNewContext(implementation.code, context, { timeout: 5000 })
|
||||
const exported = script ? context.artplayerPluginMultipleSubtitles : module.exports
|
||||
const factory = exported?.default || exported
|
||||
const latestText = async () => blobs.get(initialized.at(-1).url).text()
|
||||
return { factory, exported, art, utils, requests, blobs, revoked, initialized, converted, latestText }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Vendor parity uses the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { transformSync } from 'esbuild'
|
||||
import { checkMultipleSubtitlesVendor } from '../refactor/scripts/multiple-subtitles-vendor.mjs'
|
||||
|
||||
test('Multiple-subtitles vendor retains pinned executable content and complete standalone distribution notices', () => {
|
||||
const result = checkMultipleSubtitlesVendor()
|
||||
const notices = fs.readFileSync('packages/artplayer-plugin-multiple-subtitles/THIRD_PARTY_NOTICES', 'utf8').trim()
|
||||
const block = notices.split(/\r?\n/).map(line => line.trimEnd() ? ` * ${line.trimEnd()}` : ' *').join('\n')
|
||||
for (const suffix of ['js', 'legacy.js', 'mjs']) {
|
||||
const built = fs.readFileSync(`packages/artplayer-plugin-multiple-subtitles/dist/artplayer-plugin-multiple-subtitles.${suffix}`, 'utf8').replaceAll('\r\n', '\n')
|
||||
assert(built.includes(block), `Missing complete notice in ${suffix}`)
|
||||
assert(built.startsWith('/*!'))
|
||||
}
|
||||
assert.equal(result.provenance.revision, '380cfcce34ba8b472d3a31474874eb72a0e5f460')
|
||||
})
|
||||
|
||||
test('Multiple-subtitles vendor parser and serializer match the pinned upstream on metadata and malformed inputs', () => {
|
||||
const upstream = { exports: {}, Date: { now: () => 123 } }
|
||||
vm.runInNewContext(fs.readFileSync('refactor/baselines/multiple-subtitles-vendor/parser.txt', 'utf8'), upstream)
|
||||
const local = { exports: {}, Date: upstream.Date }
|
||||
local.module = { exports: local.exports }
|
||||
vm.runInNewContext(transformSync(fs.readFileSync('packages/artplayer-plugin-multiple-subtitles/src/parser.js', 'utf8'), { format: 'cjs' }).code, local)
|
||||
for (const input of [
|
||||
'',
|
||||
'WEBVTT\n\n0\n00:00.000 --> 00:01.000\nhello & <world>\n',
|
||||
'\uFEFFWEBVTT\r\n\r\nNOTE ignored\r\n\r\n00:01.000 --> 00:04.000 align:start\r\n<c.red>日本語</c>\r\n',
|
||||
'WEBVTT\n\nSTYLE\n::cue {color: red}\n\n00:01.000 --> 00:03.000\nB\n\n00:00.000 --> 00:04.000\nA\n',
|
||||
'WEBVTT\n\nbad --> timing\ntext\n',
|
||||
]) {
|
||||
const parse = exports => new exports.WebVTTParser().parse(input, 'metadata')
|
||||
const expected = parse(upstream.exports)
|
||||
const actual = parse(local.module.exports)
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(actual)), JSON.parse(JSON.stringify(expected)))
|
||||
const serialize = (exports, tree) => new exports.WebVTTSerializer().serialize(tree.cues, tree.styles)
|
||||
assert.equal(serialize(local.module.exports, actual), serialize(upstream.exports, expected))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Historical contracts use the repository Node runner.
|
||||
import test from 'node:test'
|
||||
import { multipleSubtitlesCandidate, multipleSubtitlesEnvironment, multipleSubtitlesHistorical, subtitleVtt } from './helpers/multiple-subtitles.js'
|
||||
|
||||
const implementations = process.env.ARTPLAYER_MULTIPLE_SUBTITLES_CANDIDATE === '1' || process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT
|
||||
? [await multipleSubtitlesCandidate()]
|
||||
: await multipleSubtitlesHistorical()
|
||||
const plain = value => JSON.parse(JSON.stringify(value))
|
||||
|
||||
for (const implementation of implementations) {
|
||||
const create = options => multipleSubtitlesEnvironment(implementation, options)
|
||||
test(`Multiple-subtitles ${implementation.name}: required factory input and async named methods remain explicit`, async () => {
|
||||
const env = create()
|
||||
assert.throws(() => env.factory(), { name: 'TypeError' })
|
||||
const register = env.factory({})
|
||||
assert.equal(typeof register, 'function')
|
||||
const pending = register(env.art)
|
||||
assert.equal(typeof pending.then, 'function')
|
||||
assert.equal(pending.name, undefined)
|
||||
assert.equal(env.initialized.length, 0)
|
||||
const result = await pending
|
||||
assert.deepEqual(Object.keys(result), ['name', 'tracks', 'reset'])
|
||||
assert.equal(result.name, 'multipleSubtitles')
|
||||
assert.equal(result.tracks(), undefined)
|
||||
assert.equal(result.reset(), undefined)
|
||||
assert.equal(await env.latestText(), 'WEBVTT\n\n')
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name}: all tracks load concurrently and retain historical merge topology`, async () => {
|
||||
const env = create({ responses: { 'en.vtt': subtitleVtt('English'), 'jp.vtt': subtitleVtt('Japanese') } })
|
||||
const pending = env.factory({ subtitles: [{ url: 'en.vtt', name: 'en' }, { url: 'jp.vtt', name: 'jp' }] })(env.art)
|
||||
assert.deepEqual(env.requests, ['en.vtt', 'jp.vtt'])
|
||||
await pending
|
||||
const text = env.utils.unescape(await env.latestText())
|
||||
assert(text.includes('<div class="art-subtitle-en">English</div>'))
|
||||
assert(text.includes('<div class="art-subtitle-jp">Japanese</div>'))
|
||||
assert.equal(text.match(/-->/g).length, implementation.version === '1.0.0' ? 1 : 2)
|
||||
assert(text.indexOf('art-subtitle-en') < text.indexOf('art-subtitle-jp'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name}: tracks select named entries, reorder, clear and reset without another fetch`, async () => {
|
||||
const env = create({ responses: { 'en.vtt': subtitleVtt('English'), 'jp.vtt': subtitleVtt('Japanese') } })
|
||||
const result = await env.factory({ subtitles: [{ url: 'en.vtt', name: 'en' }, { url: 'jp.vtt', name: 'jp' }] })(env.art)
|
||||
result.tracks(['jp'])
|
||||
assert(!env.utils.unescape(await env.latestText()).includes('art-subtitle-en'))
|
||||
result.tracks(['jp', 'en'])
|
||||
const reversed = env.utils.unescape(await env.latestText())
|
||||
assert(reversed.indexOf('art-subtitle-jp') < reversed.indexOf('art-subtitle-en'))
|
||||
result.tracks([])
|
||||
assert.equal(await env.latestText(), 'WEBVTT\n\n')
|
||||
result.reset()
|
||||
const reset = env.utils.unescape(await env.latestText())
|
||||
assert(reset.indexOf('art-subtitle-en') < reset.indexOf('art-subtitle-jp'))
|
||||
assert.equal(reset.match(/<div/g).length, 2, 'Wrapper must not accumulate on reset')
|
||||
assert.deepEqual(env.requests, ['en.vtt', 'jp.vtt'])
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name}: subtitle init replaces object URLs and retains live configuration`, async () => {
|
||||
const env = create()
|
||||
const result = await env.factory({ subtitles: [] })(env.art)
|
||||
assert.equal(env.art.option.subtitle.escape, false)
|
||||
assert.equal(env.initialized[0].onVttLoad, env.utils.unescape)
|
||||
assert.equal(env.initialized[0].style, env.art.option.subtitle.style)
|
||||
assert.equal(env.initialized[0].type, 'vtt')
|
||||
assert.deepEqual(env.revoked, [''])
|
||||
env.art.option.subtitle.encoding = 'utf-16le'
|
||||
result.reset()
|
||||
assert.equal(env.initialized[1].encoding, 'utf-16le')
|
||||
assert.deepEqual(env.revoked, ['', 'blob:subtitle-1'])
|
||||
assert.equal(env.blobs.get('blob:subtitle-2').type, 'text/vtt')
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name}: type selection dispatches existing core converters and ignores onParser`, async () => {
|
||||
let parsed = 0
|
||||
const env = create({ responses: { 'a.srt': 'source srt', 'b.ass': 'source ass', 'c.bin': subtitleVtt('Override') } })
|
||||
const onParser = () => {
|
||||
parsed++
|
||||
return {}
|
||||
}
|
||||
await env.factory({ subtitles: [{ url: 'a.srt', name: 'srt', onParser }, { url: 'b.ass', name: 'ass' }, { url: 'c.bin', name: 'override', type: 'vtt' }] })(env.art)
|
||||
assert.deepEqual(plain(env.converted), [['srt', 'source srt'], ['ass', 'source ass']])
|
||||
const text = env.utils.unescape(await env.latestText())
|
||||
for (const value of ['SRT', 'ASS', 'Override'])
|
||||
assert(text.includes(value))
|
||||
assert.equal(parsed, 0)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user