test(ci): [CI-01] cover installed Danmuku and native Mask combinations

This commit is contained in:
Harvey Zhao committed 2026-09-15 03:56:05 +08:00
1 parent 54169fdca1
commit 92b04d75c0
27 files changed
+8768 -72

No files matched your search

+3
View File
@@ -36,6 +36,9 @@ test('Installed validation rejects diagnostic SDK substitutions and recovery pro
const candidateOnly = { ...base, ARTPLAYER_MB_BROWSER_CANDIDATE: '1' }
assert.throws(() => browserInvocation('installed', [], candidateOnly), /retain MediaBunny historical controls/)
assert.equal(browserInvocation('source', [], candidateOnly).env.ARTPLAYER_MB_BROWSER_CANDIDATE, '1')
const profile = { ...base, ARTPLAYER_MASK_PROFILE: '1' }
assert.throws(() => browserInvocation('installed', [], profile), /Mask CPU profiling/)
assert.equal(browserInvocation('source', [], profile).env.ARTPLAYER_MASK_PROFILE, '1')
for (const mode of ['upstream4', 'bufferlevel4']) {
const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode }
assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/)
+5 -6
View File
@@ -2,9 +2,9 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { observeDanmukuLoad } from '../helpers/danmuku-combination-load.js'
import { dpipCandidate } from '../helpers/dpip.js'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
const dpip = await dpipCandidate()
@@ -15,10 +15,8 @@ test.beforeAll(async () => {
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', { input: `npm ${release.version}`, code: published.toString() })
const candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
implementations.set('candidate', { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', code: candidate })
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
implementations.set('candidate', { input: candidate.provenance.file || candidate.provenance.kind, ...candidate })
})
for (const core of ['published', 'candidate']) {
@@ -29,9 +27,10 @@ for (const core of ['published', 'candidate']) {
test.setTimeout(45000) // Two native PiP loads add 16 seconds of actual media progression.
const input = implementations.get(implementation)
const danmuku = input.code
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ implementation, provenance: input.provenance || { kind: 'published', sha256: hash(danmuku) }, dpip: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) } }) })
await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
const evidence = { core, implementation, ending, supported, danmuku: { input: input.input, sha256: hash(danmuku) }, dpip: { input: dpip.name, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [], loads: [] }
const evidence = { core, implementation, ending, supported, danmuku: { input: input.input, provenance: input.provenance, sha256: hash(danmuku) }, dpip: { input: dpip.name, provenance: dpip.provenance, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [], loads: [] }
if (!supported) {
evidence.outcome = 'native-document-pip-unavailable'
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
+4 -2
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
const implementations = new Map()
@@ -12,7 +12,8 @@ test.beforeAll(async () => {
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', { input: `npm ${release.version}`, code: published.toString() })
implementations.set('candidate', { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', code: process.env.ARTPLAYER_DANMUKU_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-danmuku', 'umd') })
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
implementations.set('candidate', { input: candidate.provenance.file || candidate.provenance.kind, ...candidate })
})
for (const core of ['published', 'candidate']) {
@@ -22,6 +23,7 @@ for (const core of ['published', 'candidate']) {
test.setTimeout(45000) // Three independent players, native entry/exit and timestamp-delivered rows.
await page.goto(`/test/player.html?core=${core}`)
const input = implementations.get(plugin)
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ plugin, provenance: input.provenance || { kind: 'published', sha256: hash(input.code) } }) })
const evidence = { core, plugin, mode, input: input.input, sha256: hash(input.code), cycles: [], outcome: 'incomplete' }
await page.evaluate(() => {
window.fullscreenWorkers = []
+7 -5
View File
@@ -1,14 +1,16 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let candidate
let provenance
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
;({ code: candidate, provenance } = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT))
})
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
for (const core of ['published', 'candidate']) {
+8 -6
View File
@@ -1,14 +1,16 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let candidate
let provenance
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
;({ code: candidate, provenance } = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT))
})
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
async function setup(page, core, testInfo) {
@@ -60,7 +62,7 @@ async function setup(page, core, testInfo) {
window.inputPlugin = window.art.plugins.artplayerPluginDanmuku
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await testInfo.attach('danmuku-input-build', { contentType: 'application/json', body: JSON.stringify({ core, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'in-memory source UMD', sha256: hash(candidate), worker: 'Native Blob Worker with observation only', media: '/test/pattern.mp4' }) })
await testInfo.attach('danmuku-input-build', { contentType: 'application/json', body: JSON.stringify({ core, artifact: provenance.file || provenance.kind, sha256: hash(candidate), worker: 'Native Blob Worker with observation only', media: '/test/pattern.mp4' }) })
return external
}
+6 -4
View File
@@ -2,9 +2,10 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let provenance
const implementations = new Map()
test.beforeAll(async () => {
const release = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/danmuku-release.json', import.meta.url), 'utf8')).release
@@ -12,9 +13,9 @@ test.beforeAll(async () => {
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
implementations.set('published', bytes.toString())
implementations.set('candidate', process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd'))
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
provenance = candidate.provenance
implementations.set('candidate', candidate.code)
})
for (const core of ['published', 'candidate']) {
@@ -44,6 +45,7 @@ for (const core of ['published', 'candidate']) {
}
})
const code = implementations.get(implementation)
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ implementation, provenance: implementation === 'candidate' ? provenance : { kind: 'published', sha256: hash(code) } }) })
await page.addScriptTag({ content: code })
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
await page.click('#play')
+8 -6
View File
@@ -1,14 +1,16 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let candidate
let provenance
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
;({ code: candidate, provenance } = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT))
})
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
async function setup(page, core, testInfo) {
@@ -79,7 +81,7 @@ async function setup(page, core, testInfo) {
await page.evaluate(() => window.art.playbackRate = 0.25)
await testInfo.attach('danmuku-mask-boundary-inputs', {
contentType: 'application/json',
body: JSON.stringify({ core, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(candidate), media: '/test/pattern.mp4', scope: 'Shared root-layer identity and CSS preservation with fixed inline SVG mask; native video, DOM, RAF and placement Worker. No Mask SDK/model/end-to-end acceptance. Eligible rows are explicit resource-test preconditions, not timestamp-delivery evidence.' }),
body: JSON.stringify({ core, artifact: provenance.file || provenance.kind, sha256: hash(candidate), media: '/test/pattern.mp4', scope: 'Shared root-layer identity and CSS preservation with fixed inline SVG mask; native video, DOM, RAF and placement Worker. No Mask SDK/model/end-to-end acceptance. Eligible rows are explicit resource-test preconditions, not timestamp-delivery evidence.' }),
})
return external
}
+21 -19
View File
@@ -2,42 +2,43 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { observeDanmukuLoad } from '../helpers/danmuku-combination-load.js'
import { compilePackage } from '../helpers/load.js'
import { danmukuMaskAssets } from '../helpers/danmuku-mask-assets.js'
import { expect, test } from './fixtures.js'
const artifact = process.env.ARTPLAYER_MASK_ARTIFACT || 'source UMD'
let artifact
let provenance
let danmukuProvenance
let code
const baseline = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-release.json', 'utf8'))
const assets = baseline.sdk.assets.map(({ file, sha256 }) => {
const bytes = fs.readFileSync(file)
const actual = hash(bytes)
// Git checkout may convert documentation/declaration newlines. Executed model
// scripts and binaries must retain their exact fixed bytes.
const normalizedLF = /\.(?:md|d\.ts|json)$/.test(file) && actual !== sha256
assert.equal(normalizedLF ? hash(bytes.toString().replaceAll('\r\n', '\n')) : actual, sha256, `Model asset differs from fixed source: ${file}`)
return { file, sha256: actual, baselineSha256: sha256, normalizedLF }
})
const assets = danmukuMaskAssets(process.cwd()).map(resource => resource.source)
const media = 'docs/assets/sample/steve-jobs.mp4'
const danmukuFile = process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD'
let danmukuFile
let danmukuCode
const danmukuImplementations = new Map()
test.beforeAll(async () => {
;[code, danmukuCode] = await Promise.all([
process.env.ARTPLAYER_MASK_ARTIFACT ? fs.readFileSync(artifact, 'utf8') : compilePackage('artplayer-plugin-danmuku-mask', 'umd'),
process.env.ARTPLAYER_DANMUKU_ARTIFACT ? fs.readFileSync(danmukuFile, 'utf8') : compilePackage('artplayer-plugin-danmuku', 'umd'),
const [mask, danmuku] = await Promise.all([
browserCandidate('artplayer-plugin-danmuku-mask', process.env.ARTPLAYER_MASK_ARTIFACT),
browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT),
])
code = mask.code
provenance = mask.provenance
artifact = provenance.file || provenance.kind
danmukuCode = danmuku.code
danmukuProvenance = danmuku.provenance
danmukuFile = danmukuProvenance.file || danmukuProvenance.kind
const release = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-release.json', 'utf8')).release
const member = 'package/dist/artplayer-plugin-danmuku.js'
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
danmukuImplementations.set('published', { file: `npm ${release.version}`, code: published.toString() })
danmukuImplementations.set('candidate', { file: danmukuFile, code: danmukuCode })
danmukuImplementations.set('candidate', { file: danmukuFile, code: danmukuCode, provenance: danmukuProvenance })
})
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
test(`Mask actual local MediaPipe model renders, stops and restarts with ${core} core`, async ({ page }, testInfo) => {
test.setTimeout(90000) // Includes two actual model initializations and native video decoding.
await testInfo.attach('mask-selected-input', { contentType: 'application/json', body: JSON.stringify({ core, provenance }) })
const external = []
const resources = []
const origin = new URL(testInfo.project.use.baseURL).origin
@@ -132,7 +133,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
}
finally {
const state = await page.evaluate(() => ({ snapshots: window.maskNative?.snapshots, outputs: window.maskNative?.outputs.map(({ canvas, time }) => ({ width: canvas.width, height: canvas.height, time })), video: window.art ? { time: window.art.currentTime, duration: window.art.duration, paused: window.art.video.paused, size: [window.art.video.videoWidth, window.art.video.videoHeight] } : null, mask: window.maskLayer?.style.maskImage?.slice(0, 80) }))
await testInfo.attach('actual-model-evidence', { body: JSON.stringify({ artifact, sha256: hash(code), core, assets, media: { file: media, sha256: hash(fs.readFileSync(media)) }, external, resources, state, limitation: 'Actual SDK/model, native video and output bitmap lifecycle. No assertion of private MediaPipe/GPU closure or Danmuku layout composition.' }, null, 2), contentType: 'application/json' })
await testInfo.attach('actual-model-evidence', { body: JSON.stringify({ artifact, provenance, sha256: hash(code), core, assets, media: { file: media, sha256: hash(fs.readFileSync(media)) }, external, resources, state, limitation: 'Actual SDK/model, native video and output bitmap lifecycle. No assertion of private MediaPipe/GPU closure or Danmuku layout composition.' }, null, 2), contentType: 'application/json' })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
@@ -143,6 +144,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
for (const implementation of ['published', 'candidate']) {
test(`Mask actual model and Danmuku share the layer through pause, seek and web fullscreen with ${core} core / ${implementation} plugin`, async ({ page }, testInfo) => {
const danmuku = danmukuImplementations.get(implementation)
await testInfo.attach('mask-selected-input', { contentType: 'application/json', body: JSON.stringify({ core, implementation, provenance, danmuku: danmuku.provenance || { kind: 'published', sha256: hash(danmuku.code) } }) })
test.setTimeout(90000)
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: danmuku.code })
@@ -290,7 +292,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
}
finally {
const state = await page.evaluate(() => ({ times: window.maskCombination.times, visible: window.maskCombination.visible, snapshots: window.maskCombination.snapshots, scheduled: window.maskCombination.scheduled, samples: window.maskCombination.samples, seeked: window.maskCombination.seeked, incompleteLoad: window.danmukuCombinationLoadResult }))
await testInfo.attach('actual-combination-evidence', { body: JSON.stringify({ core, implementation, mask: { file: artifact, sha256: hash(code) }, danmuku: { file: danmuku.file, sha256: hash(danmuku.code) }, unexpected, state, load, limitation: 'Actual model, native media, timestamp-delivered Danmuku and CSS web fullscreen. Bounded 20 rows/s after model readiness. No OS fullscreen, GPU completion or device coverage claim.' }, null, 2), contentType: 'application/json' })
await testInfo.attach('actual-combination-evidence', { body: JSON.stringify({ core, implementation, mask: { file: artifact, provenance, sha256: hash(code) }, danmuku: { file: danmuku.file, provenance: danmuku.provenance, sha256: hash(danmuku.code) }, unexpected, state, load, limitation: 'Actual model, native media, timestamp-delivered Danmuku and CSS web fullscreen. Bounded 20 rows/s after model readiness. No OS fullscreen, GPU completion or device coverage claim.' }, null, 2), contentType: 'application/json' })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
+8 -6
View File
@@ -1,14 +1,16 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let candidate
let provenance
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
;({ code: candidate, provenance } = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT))
})
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
async function setup(page, core, scenario, testInfo) {
@@ -102,7 +104,7 @@ async function setup(page, core, scenario, testInfo) {
document.querySelector('#play').onclick = () => window.art.play()
}, { scenario })
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await testInfo.attach('danmuku-resources-inputs', { contentType: 'application/json', body: JSON.stringify({ core, scenario, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(candidate), native: 'video, RAF, DOM, Worker; observe the first one-second timer after beforeEmit in the synchronous send continuation, no fake timer clock' }) })
await testInfo.attach('danmuku-resources-inputs', { contentType: 'application/json', body: JSON.stringify({ core, scenario, artifact: provenance.file || provenance.kind, sha256: hash(candidate), native: 'video, RAF, DOM, Worker; observe the first one-second timer after beforeEmit in the synchronous send continuation, no fake timer clock' }) })
}
test.afterEach(async ({ page }, testInfo) => {
+8 -6
View File
@@ -1,14 +1,16 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let candidate
let provenance
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd')
;({ code: candidate, provenance } = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT))
})
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
async function setup(page, core, format, scenario, testInfo) {
const external = []
@@ -109,7 +111,7 @@ async function setup(page, core, format, scenario, testInfo) {
document.querySelector('#pause').onclick = () => window.danmukuPlayers[0].pause()
}, { scenario })
await expect.poll(() => page.evaluate(() => window.art.isReady && window.art.template.$video.videoWidth > 0)).toBe(true)
await testInfo.attach('danmuku-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: 'artplayer-plugin-danmuku', format, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'in-memory source UMD', sha256: implementation.sha256, media: '/test/pattern.mp4', scenario, worker: 'Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked', heatmap: false }) })
await testInfo.attach('danmuku-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: 'artplayer-plugin-danmuku', format, artifact: provenance.file || provenance.kind, sha256: implementation.sha256, media: '/test/pattern.mp4', scenario, worker: 'Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked', heatmap: false }) })
return external
}
+6 -4
View File
@@ -2,9 +2,10 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let provenance
const implementations = new Map()
test.beforeAll(async () => {
const release = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/danmuku-release.json', import.meta.url), 'utf8')).release
@@ -12,9 +13,9 @@ test.beforeAll(async () => {
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', published.toString())
implementations.set('candidate', process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd'))
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
provenance = candidate.provenance
implementations.set('candidate', candidate.code)
})
for (const core of ['published', 'candidate']) {
@@ -26,6 +27,7 @@ for (const core of ['published', 'candidate']) {
test.setTimeout(90000)
await page.goto(`/test/player.html?core=${core}`)
const code = implementations.get(implementation)
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ implementation, provenance: implementation === 'candidate' ? provenance : { kind: 'published', sha256: hash(code) } }) })
await page.addScriptTag({ content: code })
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
await page.click('#play')
@@ -2,19 +2,20 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
const release = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/danmuku-release.json', import.meta.url), 'utf8')).release
let provenance
const implementations = new Map()
test.beforeAll(async () => {
const member = `package/${release.manifest.main.replace(/^\.\//u, '')}`
const published = readMember(await ensureArchive(release), member)
assert.equal(hash(published), release.files[member])
implementations.set('published', published.toString())
implementations.set('candidate', process.env.ARTPLAYER_DANMUKU_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-danmuku', 'umd'))
const candidate = await browserCandidate('artplayer-plugin-danmuku', process.env.ARTPLAYER_DANMUKU_ARTIFACT)
provenance = candidate.provenance
implementations.set('candidate', candidate.code)
})
for (const implementation of ['published', 'candidate']) {
@@ -22,6 +23,7 @@ for (const implementation of ['published', 'candidate']) {
test(`${implementation}: native ${scenario} records actual frame and eligibility gaps`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=candidate')
const code = implementations.get(implementation)
await testInfo.attach('danmuku-selected-input', { contentType: 'application/json', body: JSON.stringify({ implementation, provenance: implementation === 'candidate' ? provenance : { kind: 'published', sha256: hash(code) } }) })
await page.addScriptTag({ content: code })
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
await page.click('#play')
+5
View File
@@ -11,6 +11,7 @@ import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases
import { performanceHtml, performanceScript } from '../../scripts/performance-fixture.mjs'
import { getEntryFile } from '../../scripts/projects.js'
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
import { danmukuMaskAssets } from '../helpers/danmuku-mask-assets.js'
import { jassubAssets } from '../helpers/jassub-assets.js'
import { skippableVast } from './vast-fixture.mjs'
@@ -29,6 +30,10 @@ function add(route, bytes, source) {
// An explicit artifact map takes precedence over source builds and never falls back.
const mapFile = process.env.ARTPLAYER_BROWSER_ARTIFACTS
const overrides = mapFile ? JSON.parse(fs.readFileSync(mapFile, 'utf8')) : {}
if (Object.hasOwn(overrides, 'artplayer-plugin-danmuku-mask')) {
for (const resource of danmukuMaskAssets(workspace))
add(resource.route, resource.bytes, resource.source)
}
if (Object.hasOwn(overrides, 'artplayer-plugin-jassub')) {
for (const resource of jassubAssets(workspace, mapFile))
add(resource.route, resource.bytes, resource.source)
+39
View File
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Verify fixed browser resource inputs without starting the model.
import test from 'node:test'
import { hash } from '../refactor/scripts/releases.mjs'
import { danmukuMaskAssets } from './helpers/danmuku-mask-assets.js'
test('Model assets preserve executed bytes and allow only metadata newline conversion', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-mask-assets-'))
const prefix = 'docs/assets/@mediapipe/selfie_segmentation/'
const files = ['worker.js', 'model.wasm', 'package.json']
const write = (file, bytes) => {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true })
fs.writeFileSync(path.join(root, file), bytes)
}
try {
for (const file of files) write(prefix + file, 'fixture\n')
write('refactor/baselines/danmuku-mask-release.json', JSON.stringify({ sdk: { assets: files.map(file => ({ file: prefix + file, sha256: hash('fixture\n') })) } }))
const original = danmukuMaskAssets(root)
assert(original.every(item => item.source.kind === 'local-model-resource' && !item.source.normalizedLF))
assert.equal(original[0].route, '/assets/@mediapipe/selfie_segmentation/worker.js')
for (const file of files.slice(0, 2)) {
write(prefix + file, 'fixture\r\n')
assert.throws(() => danmukuMaskAssets(root), /Model asset differs/)
write(prefix + file, 'fixture\n')
}
write(`${prefix}package.json`, 'fixture\r\n')
assert.equal(danmukuMaskAssets(root)[2].source.normalizedLF, true)
fs.unlinkSync(path.join(root, `${prefix}model.wasm`))
assert.throws(() => danmukuMaskAssets(root), { code: 'ENOENT' })
}
finally {
assert.equal(path.dirname(root), path.resolve(os.tmpdir()))
assert(path.basename(root).startsWith('artplayer-mask-assets-'))
fs.rmSync(root, { recursive: true, force: true })
}
})
+18
View File
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { hash } from '../../refactor/scripts/releases.mjs'
export function danmukuMaskAssets(root) {
const baseline = JSON.parse(fs.readFileSync(path.join(root, 'refactor/baselines/danmuku-mask-release.json'), 'utf8'))
return baseline.sdk.assets.map(({ file, sha256 }) => {
assert(file.startsWith('docs/assets/@mediapipe/selfie_segmentation/') && !file.split('/').includes('..'), 'Unexpected model asset path')
const absolute = path.join(root, file)
const bytes = fs.readFileSync(absolute)
const actual = hash(bytes)
// Checkout newline conversion is allowed only for metadata, never executed assets.
const normalizedLF = /\.(?:md|d\.ts|json)$/.test(file) && actual !== sha256
assert.equal(normalizedLF ? hash(bytes.toString().replaceAll('\r\n', '\n')) : actual, sha256, `Model asset differs from fixed source: ${file}`)
return { route: file.slice(4), bytes, source: { kind: 'local-model-resource', file: absolute, relativeFile: file, sha256: actual, baselineSha256: sha256, normalizedLF } }
})
}
+1 -1
View File
@@ -61,7 +61,7 @@ test('Explicit installed plugin maps never fall back to source or frozen workspa
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE
process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT = 'override.js'
await assert.rejects(autoThumbnailCandidate(), /cannot override artplayer-plugin-auto-thumbnail artifact/)
for (const name of ['artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-plugin-jassub']) {
for (const name of ['artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-plugin-jassub', 'artplayer-plugin-danmuku', 'artplayer-plugin-danmuku-mask']) {
await assert.rejects(browserCandidate(name), { code: 'ENOENT' })
await assert.rejects(browserCandidate(name, 'override.js'), new RegExp(`cannot override ${name} artifact`))
}