test(ci): [CI-01] verify installed JASSUB bundles and worker resources

This commit is contained in:
Harvey Zhao committed 2026-09-15 03:20:10 +08:00
1 parent 25a0c6303d
commit 54169fdca1
19 files changed
+2500 -217

No files matched your search

@@ -167,3 +167,24 @@ likewise a capability fallback control. Further codec/device/hybrid combinations
and sustained resource release remain PKG-JASSUB-05.
See
`../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence.
## Installed browser inputs
`yarn test:package --browser` includes this package and its worker directory. Set
ARTPLAYER_BROWSER_ARTIFACTS to the generated map and run
`yarn test:browser:installed jassub --workers=2`. The installed matrix retains
the published native-render control and adds the installed candidate for all three
core versions. Lifecycle, hybrid and render-failure suites use the verified
installed candidate; without a map they build current source, unless an explicit
artifact is selected. The native-render suite retains its historical default in
source mode. The platform suite has no JASSUB wrapper/WASM and is a control only.
The browser server maps the three worker/WASM URLs to installed package files,
checking archive digests and frozen resource baselines. The font remains a separate
local resource with its own fixed digest; it is not packaged by this plugin.
Worker source inventory joins src/public freshness checks. Actual HTTP response
and byte identities are exposed in the server manifest; a missing/mutated installed
worker cannot fall back to docs assets. These routes preserve the test URLs and
do not change package exports, distributed bytes, resource defaults or licensing.
Published WebKit rendering defects and physical/GPU/notice gates remain distinct
from candidate results; see the CI checkpoint for actual execution.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,53 @@
# CI-01 JASSUB 安装包与 Worker/WASM 资源验证
## 范围
共同清单加入 JASSUB 五个浏览器文件。native 渲染在安装模式保留真实 npm
历史 wrapper,并加入安装候选,分别覆盖三个核心。原有 source 命令默认
历史 wrapper 的含义不变;其余三组 wrapper 回归默认构建源码,安装模式
必须读取核验过的安装文件,拒绝冲突 artifact。platform 文件不加载
JASSUB,仅是原生 Canvas/视频的对照。
安装服务将 worker JS、两个 WASM URL 映射到 tarball 安装后的 worker 目录,
核对冻结资源及归档成员摘要。字体仍来自 docs 的固定外部资源,并单列摘要,
不能声称字体在 npm 包中。旧 wrapper 与候选使用相同固定 worker 字节;
没有升级外部资源或修改用户 URL。worker 目录同时加入源输入新鲜度校验。
新增负例验证 worker 源码变化、安装 worker 替换、字体漂移不能静默通过,
服务器 manifest 保留实际资源内容和来源。没有生产实现/依赖/类型/分发入口
变更,也没有改变原来的失败断言与超时。
## 实测
Windows x64 / Node 24.21.0 / Yarn 1.22.22。十六包正常构建/pack/隔离安装和
离线冻结复装通过,run-JYQlOv,63.18 秒,实际退出 0。通用消费者仍仅
core/chapter 的 36 运行时、5 旧类型和 8 精确类型模式;不代表 JASSUB 全部
消费者/许可验收。完整安装清单收集 54 文件/1581 项,本轮只执行新增五文件。
Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6 共 45 项,41 通过、4 失败,
0 skip/重试,63.575 秒,实际退出 1。候选 27 项中 26 通过:三核心、三引擎
的显式 offscreenRender=false 原生字幕组合 9/9 通过;Firefox hybrid 在首次
像素读取等待处失败,尚未进入延迟位图交付分支,关联既有
JASSUB-FIREFOX-OFFSCREEN-01,不宣称新的安装包问题或已修复。另三项是
实际 npm 1.1.0 在 WebKit 三核心组合的历史零帧计数问题,候选对应项通过。
九项无 JASSUB 的原生平台对照通过,不能计为字幕功能通过;hybrid 的能力
回退也不能算完整离屏/颜色空间验证。没有把任何失败转换成 skip 或放宽等待。
每个安装模式 browser-evidence 的 manifest 均核验四个资源摘要:三个 worker/
WASM 来自实际安装文件并与归档成员一致,字体来自单独本地资源;原生字幕
用例另外记录实际 HTTP 响应。manifest 可用资源清单不等同每个用例都请求
全部资源,更不声称两个 WASM 分支在单次渲染中同时使用。
故意继承 map 后 source 入口清除它;Chromium 视频替换、hybrid、绘制失败
恢复三项 3/3,通过附件证明 source-build,32.948 秒,实际退出 0。相关候选
单元 84/84、工程 54/54、scoped lint、library 类型、严格工具链和 CI 契约通过。
初始 lint 的 Buffer 导入与循环缩进已修正,相关日志保留。工程负例还验证
即使本地字节符合基线,归档缺少 worker 成员仍会拒绝。
原始报告在实际进程结束后归档为 ci01-jassub-installed-report、
ci01-jassub-source-report 和 ci01-jassub-collection-report。命令、逐例状态、
失败现场、资源/源码/归档摘要及日志指纹见
[机器证据](../baselines/ci-jassub-installed-validation.json)。CI-01/完整 JASSUB
设备和发布门槛保持开放,
不以本地安装覆盖代替物理设备、持续 GPU/内存或许可材料审查。
回退本检查点恢复十五包安装清单和原有 docs 资源加载,不影响播放器 API。
+1 -1
View File
@@ -509,7 +509,7 @@
- ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.json)
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
- ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json) [记录](changes/2026-09-15-CI-01-jassub-installed.md) [记录](baselines/ci-jassub-installed-validation.json)
- CI-02: [记录](changes/2026-09-14-CI-02-pages-artifact.md) [记录](baselines/pages-artifact-validation.json) [记录](pages-deployment.md)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## CI-01 JASSUB 主包及安装 Worker/WASM 验证
十六包打包/安装通过,JASSUB 五文件实跑 45 项:41 通过、4 失败,实际退出 1。
三项是旧版 WebKit 渲染对照;另一项是已有 Firefox 离屏读回停顿,风险保持开放。
候选 26/27,通过的九项原生字幕三核心/三引擎组合与资源来源均有证据,
字体作为独立资源记录。源码定向 3/3、候选单元 84/84、工程 54/54 通过,
见[记录](changes/2026-09-15-CI-01-jassub-installed.md)。完整安装清单为
54 文件/1581 项,本轮未全跑。197/263 完成;继续弹幕及其 Mask 资源映射,
不关闭 Firefox、设备、许可或全量 CI 门槛。
## CI-01 MediaBunny 接入实际安装矩阵
十五包打包/隔离安装通过,新增 MediaBunny 十文件实跑 228/228 断言通过,
+3 -1
View File
@@ -5779,7 +5779,9 @@
"refactor/baselines/jassub-display-validation.json",
"test/browser/jassub-platform.spec.js",
"test/browser/jassub-native.spec.js",
"test/browser/jassub-hybrid.spec.js"
"test/browser/jassub-hybrid.spec.js",
"refactor/changes/2026-09-15-CI-01-jassub-installed.md",
"refactor/baselines/ci-jassub-installed-validation.json"
],
"compatibleResolution": "Isolate native media/compositor/readback and sequence effects without globally disabling default offscreen or deleting assertions. The pre-09 artifact also fails in the same sequence; do not claim the new hybrid guard caused or fixed this separate failure.",
"closureCriteria": "Identify and verify a compatible fix or a reviewed precise platform boundary with native controls. Preserve known red sequence and successful standalone comparison; candidate playback/fullscreen pixels and lifetime must be justified, not merely rerun to green."
+3 -1
View File
@@ -772,7 +772,9 @@
"changes/2026-09-15-CI-01-asr-cast-installed.md",
"baselines/ci-asr-cast-installed-validation.json",
"changes/2026-09-15-CI-01-mediabunny-installed.md",
"baselines/ci-mediabunny-installed-validation.json"
"baselines/ci-mediabunny-installed-validation.json",
"changes/2026-09-15-CI-01-jassub-installed.md",
"baselines/ci-jassub-installed-validation.json"
]
},
{
+12
View File
@@ -106,3 +106,15 @@ Available-input attachments list possible inputs; per-observation provenance
identifies the selected one. Native media/decoding, controlled races, API absence
and physical/long-run verification remain separate. See
../../refactor/changes/2026-09-15-CI-01-mediabunny-installed.md for actual results.
JASSUB adds its native rendering, lifecycle, hybrid, render-failure and platform
control files. The native-render file retains published inputs and adds installed
candidates; its source-mode historical default stays unchanged. Three other
wrapper suites build source unless an installed map or explicit artifact selects
them. Installed worker JS and both WASM files are served at the existing URLs,
with archived-member and frozen-resource hashes verified before serving. The
local font is identified separately; it is not part of the plugin tarball.
Package worker inventories now participate in input freshness checks. The
platform control loads no JASSUB; historical failures, capability controls and
actual rendering must be reported separately. See
../../refactor/changes/2026-09-15-CI-01-jassub-installed.md.
+6 -1
View File
@@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'
import assert from 'node:assert/strict'
export type BrowserScope = 'source' | 'installed'
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-proxy-mediabunny']
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-proxy-mediabunny', 'artplayer-plugin-jassub']
export const installedTests = [
'playback.spec.js',
'lifecycle.spec.js',
@@ -53,6 +53,11 @@ export const installedTests = [
'mediabunny-shim.spec.js',
'mediabunny-video.spec.js',
'mediabunny.spec.js',
'jassub-native.spec.js',
'jassub-lifecycle.spec.js',
'jassub-hybrid.spec.js',
'jassub-render-failure.spec.js',
'jassub-platform.spec.js',
]
export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
+1 -1
View File
@@ -40,7 +40,7 @@ export function verifyInstalledArtifacts(root, mapFile, names = ['artplayer', 'a
const member = `package/dist/${path.basename(file)}`
assert.equal(hash(fs.readFileSync(file)), pkg.files[member], `Installed browser artifact changed: ${name}`)
const sourceHashes = {}
for (const folder of ['src', 'public']) {
for (const folder of ['src', 'public', 'worker']) {
const current = path.join(root, 'packages', name, folder)
const snapshot = path.join(directory, 'build/packages', name, folder)
if (!fs.existsSync(current)) {
+1
View File
@@ -96,6 +96,7 @@ export async function checkPackages({ release = false, include = [] } = {}) {
'artplayer-plugin-asr': async () => (await import('../refactor/scripts/asr-contract.mjs')).verifyAsrContract(),
'artplayer-plugin-chromecast': async () => (await import('../refactor/scripts/chromecast-contract.mjs')).verifyChromecastContract(),
'artplayer-proxy-mediabunny': async () => (await import('../refactor/scripts/mb-contract.mjs')).verifyMbContract(),
'artplayer-plugin-jassub': async () => (await import('../refactor/scripts/jassub-contract.mjs')).verifyJassubContract(),
'artplayer-plugin-audio-track': async () => (await import('../refactor/scripts/audio-contract.mjs')).verifyAudioContract(),
'artplayer-plugin-vtt-thumbnail': async () => (await import('../refactor/scripts/vtt-thumbnail-contract.mjs')).verifyVttThumbnailContract(),
'artplayer-plugin-multiple-subtitles': async () => (await import('../refactor/scripts/multiple-subtitles-contract.mjs')).verifyMultipleSubtitlesContract(),
+8 -4
View File
@@ -1,10 +1,14 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT || 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
const code = fs.readFileSync(artifact, 'utf8')
const { code, provenance } = await browserCandidate('artplayer-plugin-jassub', process.env.ARTPLAYER_JASSUB_ARTIFACT)
const artifact = provenance.file || 'source-build'
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('jassub-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
const subtitle = `[Script Info]
ScriptType: v4.00+
PlayResX: 640
@@ -152,7 +156,7 @@ test('JASSUB actual subtitle color-space transitions tolerate a queued native hy
}
return { available: probe.available, selected: probe.selected, matrix: probe.matrix, sent: probe.sent, received: probe.received, late: probe.late, terminal: probe.terminal, errors: probe.errors, busy: window.jassub.busy, lastDemand: window.jassub._lastDemandTime }
})
await testInfo.attach('jassub-native-hybrid', { body: JSON.stringify({ artifact, sha256: hash(code), state, limitation: 'Actual Worker/WASM/ASS color-space transition and native ImageBitmap; one received render is held and delivered after synchronous public setTrack to reproduce a queued message. Not unmodified event timing, GPU endurance or physical-device evidence.' }), contentType: 'application/json' })
await testInfo.attach('jassub-native-hybrid', { body: JSON.stringify({ artifact, provenance, sha256: hash(code), state, limitation: 'Actual Worker/WASM/ASS color-space transition and native ImageBitmap; one received render is held and delivered after synchronous public setTrack to reproduce a queued message. Not unmodified event timing, GPU endurance or physical-device evidence.' }), contentType: 'application/json' })
await page.evaluate(() => {
if (!window.art.isDestroy)
window.art.destroy()
+10 -6
View File
@@ -1,10 +1,14 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT || 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
const code = fs.readFileSync(artifact, 'utf8')
const { code, provenance } = await browserCandidate('artplayer-plugin-jassub', process.env.ARTPLAYER_JASSUB_ARTIFACT)
const artifact = provenance.file || 'source-build'
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('jassub-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
const subtitles = `[Script Info]
ScriptType: v4.00+
PlayResX: 640
@@ -116,7 +120,7 @@ for (const onDemandRender of [true, false]) {
expect(state.errors).toEqual([])
if (!onDemandRender)
expect(state.sent.some(message => message.target === 'video' && message.rate === 1.5)).toBe(true)
await testInfo.attach('jassub-native-lifecycle', { body: JSON.stringify({ artifact, sha256: hash(code), onDemandRender, state }), contentType: 'application/json' })
await testInfo.attach('jassub-native-lifecycle', { body: JSON.stringify({ artifact, provenance, sha256: hash(code), onDemandRender, state }), contentType: 'application/json' })
}
finally {
await page.evaluate(() => {
@@ -158,7 +162,7 @@ test('JASSUB actual invalid Worker URL construction releases the newly created c
expect(state.error?.name).toBe('SyntaxError')
expect(state.activeListeners).toBe(0)
expect(state.ownedContainers).toBe(0)
await testInfo.attach('jassub-native-construction-failure', { body: JSON.stringify({ artifact, sha256: hash(code), state }), contentType: 'application/json' })
await testInfo.attach('jassub-native-construction-failure', { body: JSON.stringify({ artifact, provenance, sha256: hash(code), state }), contentType: 'application/json' })
})
test('JASSUB native asynchronous CSP Worker error reaches a pending query and explicit destruction cleans up', async ({ page }, testInfo) => {
@@ -186,5 +190,5 @@ test('JASSUB native asynchronous CSP Worker error reaches a pending query and ex
expect(state.calls).toBe(1)
expect(state.terminated).toBe(1)
expect(state.ownedContainers).toBe(0)
await testInfo.attach('jassub-native-worker-error', { body: JSON.stringify({ artifact, sha256: hash(code), state }), contentType: 'application/json' })
await testInfo.attach('jassub-native-worker-error', { body: JSON.stringify({ artifact, provenance, sha256: hash(code), state }), contentType: 'application/json' })
})
+205 -195
View File
@@ -1,6 +1,7 @@
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 { expect, test } from './fixtures.js'
import { captureJassubDisplay } from './jassub-display.js'
@@ -8,9 +9,14 @@ const baseline = JSON.parse(fs.readFileSync('refactor/baselines/jassub-release.j
const release = baseline.release
const archive = await ensureArchive(release)
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT
const explicitArtifact = process.env.ARTPLAYER_JASSUB_ARTIFACT
const publishedCode = readMember(archive, member).toString()
if (hash(publishedCode) !== release.files[member])
throw new Error('Published JASSUB artifact changed')
const published = { code: publishedCode, provenance: { kind: 'published', version: release.version, integrity: release.integrity, member, sha256: hash(publishedCode) } }
const candidate = process.env.ARTPLAYER_BROWSER_ARTIFACTS || explicitArtifact ? await browserCandidate('artplayer-plugin-jassub', explicitArtifact) : null
const inputs = process.env.ARTPLAYER_BROWSER_ARTIFACTS ? [published, candidate] : [candidate || published]
const customCanvas = process.env.ARTPLAYER_JASSUB_CUSTOM_CANVAS === 'true'
const code = artifact ? fs.readFileSync(artifact, 'utf8') : readMember(archive, member).toString()
const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
const defaultOffscreen = process.env.ARTPLAYER_JASSUB_OFFSCREEN === 'default'
const readbackFrame = process.env.ARTPLAYER_JASSUB_READBACK_FRAME === 'true'
@@ -50,202 +56,206 @@ Dialogue: 0,${screenshotReadback ? '0:00:40.00,0:01:50.00' : '0:00:05.00,0:00:30
`
const testedSubtitles = screenshotReadback ? subtitles.replace('&H00FFFFFF', '&H0000FF00') : subtitles
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
test(`JASSUB ${artifact ? 'candidate artifact' : `published ${release.version}`} renders actual WASM subtitles through seek and layout with ${core} core, customCanvas=${customCanvas}, defaultOffscreen=${defaultOffscreen}`, async ({ page, context }, testInfo) => {
const resources = []
const external = []
const origin = new URL(testInfo.project.use.baseURL).origin
await context.route('**/*', (route) => {
if (new URL(route.request().url()).origin !== origin) {
external.push(route.request().url())
return route.abort()
for (const { code, provenance } of inputs) {
const artifact = provenance.kind !== 'published' ? provenance.file : null
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
test(`JASSUB ${artifact ? 'candidate artifact' : `published ${release.version}`} renders actual WASM subtitles through seek and layout with ${core} core, customCanvas=${customCanvas}, defaultOffscreen=${defaultOffscreen}`, async ({ page, context }, testInfo) => {
await testInfo.attach('jassub-selected-input', { contentType: 'application/json', body: JSON.stringify({ core, provenance }) })
const resources = []
const external = []
const origin = new URL(testInfo.project.use.baseURL).origin
await context.route('**/*', (route) => {
if (new URL(route.request().url()).origin !== origin) {
external.push(route.request().url())
return route.abort()
}
return route.continue()
})
context.on('response', (response) => {
if (response.url().includes('/assets/jassub/'))
resources.push({ url: response.url(), status: response.status(), contentType: response.headers()['content-type'] })
})
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => {
window.jassubFramePlatform = { before: typeof HTMLVideoElement.prototype.requestVideoFrameCallback, registered: 0, delivered: 0 }
})
await page.addScriptTag({ content: code })
await page.evaluate(() => {
const frames = window.jassubFramePlatform
frames.after = typeof HTMLVideoElement.prototype.requestVideoFrameCallback
if (frames.after === 'function') {
const request = HTMLVideoElement.prototype.requestVideoFrameCallback
HTMLVideoElement.prototype.requestVideoFrameCallback = function (callback) {
frames.registered++
return request.call(this, function (...args) {
frames.delivered++
frames.lastMetadata = args[1]
return callback.apply(this, args)
})
}
}
window.jassubNative = { workers: [], snapshots: [], errors: [], frames }
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.observation = { url: String(args[0]), terminated: 0, sent: [], received: [] }
window.jassubNative.workers.push(this.observation)
this.addEventListener('message', ({ data }) => this.observation.received.push({ target: data.target, images: data.images?.length, width: data.width, height: data.height }))
}
postMessage(message, ...rest) {
this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height, asyncRender: message.asyncRender })
return super.postMessage(message, ...rest)
}
terminate() {
this.observation.terminated++
return super.terminate()
}
}
window.createPlayer('/assets/sample/steve-jobs.mp4')
})
try {
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await page.evaluate(async ({ subContent, onDemandRender, customCanvas, defaultOffscreen, synchronousRender }) => {
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
let canvas
if (customCanvas) {
canvas = document.createElement('canvas')
canvas.style.position = 'absolute'
canvas.style.pointerEvents = 'none'
canvas.style.zIndex = '20'
window.art.video.insertAdjacentElement('afterend', canvas)
window.jassubUserCanvas = canvas
}
window.art.plugins.add(register({
subContent,
workerUrl: '/assets/jassub/jassub-worker.js',
wasmUrl: '/assets/jassub/jassub-worker.wasm',
modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
fonts: ['/assets/jassub/default.woff2'],
...(defaultOffscreen ? {} : { offscreenRender: false }),
...(synchronousRender ? { asyncRender: false } : {}),
onDemandRender,
...(canvas ? { canvas } : {}),
}))
window.jassub = window.art.plugins.artplayerPluginJassub.instance
window.jassubNative.renderCalls = []
const render = window.jassub._render
window.jassub._render = function (message) {
window.jassubNative.renderCalls.push({ context: Boolean(this._ctx), images: message.images.length })
return render.call(this, message)
}
window.jassubNative.offscreen = { available: 'transferControlToOffscreen' in HTMLCanvasElement.prototype, selected: window.jassub._offscreenRender }
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
await new Promise((resolve, reject) => {
window.jassub.addEventListener('ready', resolve, { once: true })
window.jassub.addEventListener('error', event => reject(event.error), { once: true })
})
const readback = document.createElement('canvas')
window.jassubGeometry = () => {
const canvas = window.jassub._canvas
const rect = canvas.getBoundingClientRect()
const video = window.art.video
const quality = video.getVideoPlaybackQuality?.()
return { width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, clip: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, time: window.art.currentTime, connected: canvas.isConnected, media: { paused: video.paused, readyState: video.readyState, totalFrames: quality?.totalVideoFrames, droppedFrames: quality?.droppedVideoFrames, decodedFrames: video.webkitDecodedFrameCount, mozPresentedFrames: video.mozPresentedFrames }, onDemand: window.jassub._onDemandRender }
}
window.jassubPixels = (phase) => {
const canvas = window.jassub._canvas
readback.width = canvas.width
readback.height = canvas.height
const context = readback.getContext('2d')
context.drawImage(canvas, 0, 0)
const pixels = context.getImageData(0, 0, readback.width, readback.height).data
let visible = 0
let signature = 0
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] > 0) {
visible++
signature = (signature + i * pixels[i]) >>> 0
}
}
const state = { phase, visible, signature, ...window.jassubGeometry() }
window.jassubNative.lastPixel = state
if (phase)
window.jassubNative.snapshots.push(state)
return state
}
}, { subContent: testedSubtitles, onDemandRender, customCanvas, defaultOffscreen, synchronousRender })
const offscreen = await page.evaluate(() => window.jassubNative.offscreen)
expect(offscreen.selected).toBe(defaultOffscreen && !customCanvas && offscreen.available)
await page.locator('#play').click()
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
const before = await pixels(page, 'before-seek')
expect(before.connected).toBe(true)
if (screenshotReadback) {
expect(before.time).toBeLessThan(30)
expect(before.visible).toBeGreaterThan(100)
}
await page.locator('#pause').click()
await page.evaluate(async (seekTime) => {
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
window.art.currentTime = seekTime
await seeked
}, seekTime)
await page.locator('#play').click()
await expect.poll(async () => (await pixels(page)).signature).not.toBe(before.signature)
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
const after = await pixels(page, 'after-seek')
expect(after.time).toBeGreaterThanOrEqual(seekTime)
expect(after.signature).not.toBe(before.signature)
await page.evaluate(() => {
window.art.fullscreenWeb = true
})
await expect.poll(async () => (await pixels(page)).cssWidth).toBeGreaterThan(before.cssWidth)
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
await pixels(page, 'fullscreen')
if (screenshotReadback) {
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(after.time + 0.2)
await page.evaluate(() => {
window.art.pause()
window.jassub._canvas.style.visibility = 'hidden'
})
const hidden = await pixels(page, 'hidden-negative-control')
expect(hidden.visible).toBe(0)
await page.evaluate(() => window.jassub._canvas.style.visibility = '')
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
await pixels(page, 'restored')
}
await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
if (artifact)
await page.evaluate(() => window.jassub.destroy())
await page.evaluate(() => window.art.destroy(false))
if (customCanvas)
expect(await page.evaluate(() => window.jassubUserCanvas.isConnected)).toBe(true)
expect(await page.evaluate(() => document.querySelectorAll('.JASSUB').length)).toBe(0)
expect(await page.evaluate(() => window.jassubNative.workers.map(worker => worker.terminated))).toEqual([1])
expect(await page.evaluate(() => window.jassubNative.errors)).toEqual([])
expect(external).toEqual([])
expect(resources.some(item => item.url.endsWith('.wasm') && item.status === 200 && item.contentType === 'application/wasm')).toBe(true)
expect(resources.some(item => item.url.endsWith('.woff2') && item.status === 200)).toBe(true)
}
return route.continue()
})
context.on('response', (response) => {
if (response.url().includes('/assets/jassub/'))
resources.push({ url: response.url(), status: response.status(), contentType: response.headers()['content-type'] })
})
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => {
window.jassubFramePlatform = { before: typeof HTMLVideoElement.prototype.requestVideoFrameCallback, registered: 0, delivered: 0 }
})
await page.addScriptTag({ content: code })
await page.evaluate(() => {
const frames = window.jassubFramePlatform
frames.after = typeof HTMLVideoElement.prototype.requestVideoFrameCallback
if (frames.after === 'function') {
const request = HTMLVideoElement.prototype.requestVideoFrameCallback
HTMLVideoElement.prototype.requestVideoFrameCallback = function (callback) {
frames.registered++
return request.call(this, function (...args) {
frames.delivered++
frames.lastMetadata = args[1]
return callback.apply(this, args)
finally {
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
state.readbackFrame = readbackFrame
state.synchronousRender = synchronousRender
state.screenshotReadback = screenshotReadback
const observation = screenshotReadback
? 'Composited page PNG decoded in Node; authored green ASS glyphs with hidden/restored negative control. No transferred-canvas copy.'
: 'Canvas pixels are read from a separate canvas copying the native display bitmap.'
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, defaultOffscreen, source: provenance, subtitleSha256: hash(testedSubtitles), resources, external, state, limitation: `Actual worker, WASM and local font; offscreen option ${defaultOffscreen ? 'omitted, actual capability/selection recorded' : 'explicitly false'}. ${observation} No full failure recovery, sustained GPU/memory or physical-device acceptance; candidate adds direct-then-host destruction.` }, null, 2), contentType: 'application/json' })
if (!page.isClosed()) {
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
}
}
window.jassubNative = { workers: [], snapshots: [], errors: [], frames }
const NativeWorker = window.Worker
window.Worker = class extends NativeWorker {
constructor(...args) {
super(...args)
this.observation = { url: String(args[0]), terminated: 0, sent: [], received: [] }
window.jassubNative.workers.push(this.observation)
this.addEventListener('message', ({ data }) => this.observation.received.push({ target: data.target, images: data.images?.length, width: data.width, height: data.height }))
}
postMessage(message, ...rest) {
this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height, asyncRender: message.asyncRender })
return super.postMessage(message, ...rest)
}
terminate() {
this.observation.terminated++
return super.terminate()
}
}
window.createPlayer('/assets/sample/steve-jobs.mp4')
})
try {
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await page.evaluate(async ({ subContent, onDemandRender, customCanvas, defaultOffscreen, synchronousRender }) => {
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
let canvas
if (customCanvas) {
canvas = document.createElement('canvas')
canvas.style.position = 'absolute'
canvas.style.pointerEvents = 'none'
canvas.style.zIndex = '20'
window.art.video.insertAdjacentElement('afterend', canvas)
window.jassubUserCanvas = canvas
}
window.art.plugins.add(register({
subContent,
workerUrl: '/assets/jassub/jassub-worker.js',
wasmUrl: '/assets/jassub/jassub-worker.wasm',
modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
fonts: ['/assets/jassub/default.woff2'],
...(defaultOffscreen ? {} : { offscreenRender: false }),
...(synchronousRender ? { asyncRender: false } : {}),
onDemandRender,
...(canvas ? { canvas } : {}),
}))
window.jassub = window.art.plugins.artplayerPluginJassub.instance
window.jassubNative.renderCalls = []
const render = window.jassub._render
window.jassub._render = function (message) {
window.jassubNative.renderCalls.push({ context: Boolean(this._ctx), images: message.images.length })
return render.call(this, message)
}
window.jassubNative.offscreen = { available: 'transferControlToOffscreen' in HTMLCanvasElement.prototype, selected: window.jassub._offscreenRender }
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
await new Promise((resolve, reject) => {
window.jassub.addEventListener('ready', resolve, { once: true })
window.jassub.addEventListener('error', event => reject(event.error), { once: true })
})
const readback = document.createElement('canvas')
window.jassubGeometry = () => {
const canvas = window.jassub._canvas
const rect = canvas.getBoundingClientRect()
const video = window.art.video
const quality = video.getVideoPlaybackQuality?.()
return { width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, clip: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, time: window.art.currentTime, connected: canvas.isConnected, media: { paused: video.paused, readyState: video.readyState, totalFrames: quality?.totalVideoFrames, droppedFrames: quality?.droppedVideoFrames, decodedFrames: video.webkitDecodedFrameCount, mozPresentedFrames: video.mozPresentedFrames }, onDemand: window.jassub._onDemandRender }
}
window.jassubPixels = (phase) => {
const canvas = window.jassub._canvas
readback.width = canvas.width
readback.height = canvas.height
const context = readback.getContext('2d')
context.drawImage(canvas, 0, 0)
const pixels = context.getImageData(0, 0, readback.width, readback.height).data
let visible = 0
let signature = 0
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] > 0) {
visible++
signature = (signature + i * pixels[i]) >>> 0
}
}
const state = { phase, visible, signature, ...window.jassubGeometry() }
window.jassubNative.lastPixel = state
if (phase)
window.jassubNative.snapshots.push(state)
return state
}
}, { subContent: testedSubtitles, onDemandRender, customCanvas, defaultOffscreen, synchronousRender })
const offscreen = await page.evaluate(() => window.jassubNative.offscreen)
expect(offscreen.selected).toBe(defaultOffscreen && !customCanvas && offscreen.available)
await page.locator('#play').click()
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
const before = await pixels(page, 'before-seek')
expect(before.connected).toBe(true)
if (screenshotReadback) {
expect(before.time).toBeLessThan(30)
expect(before.visible).toBeGreaterThan(100)
}
await page.locator('#pause').click()
await page.evaluate(async (seekTime) => {
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
window.art.currentTime = seekTime
await seeked
}, seekTime)
await page.locator('#play').click()
await expect.poll(async () => (await pixels(page)).signature).not.toBe(before.signature)
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
const after = await pixels(page, 'after-seek')
expect(after.time).toBeGreaterThanOrEqual(seekTime)
expect(after.signature).not.toBe(before.signature)
await page.evaluate(() => {
window.art.fullscreenWeb = true
})
await expect.poll(async () => (await pixels(page)).cssWidth).toBeGreaterThan(before.cssWidth)
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
await pixels(page, 'fullscreen')
if (screenshotReadback) {
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(after.time + 0.2)
await page.evaluate(() => {
window.art.pause()
window.jassub._canvas.style.visibility = 'hidden'
})
const hidden = await pixels(page, 'hidden-negative-control')
expect(hidden.visible).toBe(0)
await page.evaluate(() => window.jassub._canvas.style.visibility = '')
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
await pixels(page, 'restored')
}
await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
if (artifact)
await page.evaluate(() => window.jassub.destroy())
await page.evaluate(() => window.art.destroy(false))
if (customCanvas)
expect(await page.evaluate(() => window.jassubUserCanvas.isConnected)).toBe(true)
expect(await page.evaluate(() => document.querySelectorAll('.JASSUB').length)).toBe(0)
expect(await page.evaluate(() => window.jassubNative.workers.map(worker => worker.terminated))).toEqual([1])
expect(await page.evaluate(() => window.jassubNative.errors)).toEqual([])
expect(external).toEqual([])
expect(resources.some(item => item.url.endsWith('.wasm') && item.status === 200 && item.contentType === 'application/wasm')).toBe(true)
expect(resources.some(item => item.url.endsWith('.woff2') && item.status === 200)).toBe(true)
}
finally {
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
state.readbackFrame = readbackFrame
state.synchronousRender = synchronousRender
state.screenshotReadback = screenshotReadback
const observation = screenshotReadback
? 'Composited page PNG decoded in Node; authored green ASS glyphs with hidden/restored negative control. No transferred-canvas copy.'
: 'Canvas pixels are read from a separate canvas copying the native display bitmap.'
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, defaultOffscreen, source: artifact ? { artifact, sha256: hash(code) } : { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(testedSubtitles), resources, external, state, limitation: `Actual worker, WASM and local font; offscreen option ${defaultOffscreen ? 'omitted, actual capability/selection recorded' : 'explicitly false'}. ${observation} No full failure recovery, sustained GPU/memory or physical-device acceptance; candidate adds direct-then-host destruction.` }, null, 2), contentType: 'application/json' })
if (!page.isClosed()) {
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
}
}
})
}
}
+8 -4
View File
@@ -1,10 +1,14 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT || 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
const code = fs.readFileSync(artifact, 'utf8')
const { code, provenance } = await browserCandidate('artplayer-plugin-jassub', process.env.ARTPLAYER_JASSUB_ARTIFACT)
const artifact = provenance.file || 'source-build'
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('jassub-selected-input', { contentType: 'application/json', body: JSON.stringify({ browserName, provenance }) })
})
const subtitle = `[Script Info]
ScriptType: v4.00+
PlayResX: 640
@@ -76,7 +80,7 @@ test('JASSUB closes the entire bitmap batch after a native draw failure and rend
probe.holding = false
return { before, after, failure, asyncRender: event.data.asyncRender }
})
await testInfo.attach('bitmap-failure', { contentType: 'application/json', body: JSON.stringify({ artifact, sha256: hash(code), evidence, limitation: 'Actual JASSUB Worker/WASM bitmap, plus two native bitmap copies. Deliberately closing the first bitmap injects a native draw error; this does not claim spontaneous Worker corruption or Firefox offscreen recovery.' }) })
await testInfo.attach('bitmap-failure', { contentType: 'application/json', body: JSON.stringify({ artifact, provenance, sha256: hash(code), evidence, limitation: 'Actual JASSUB Worker/WASM bitmap, plus two native bitmap copies. Deliberately closing the first bitmap injects a native draw error; this does not claim spontaneous Worker corruption or Firefox offscreen recovery.' }) })
expect(evidence.before.every(width => width > 0)).toBe(true)
expect(evidence.asyncRender).toBe(true)
expect(evidence.failure?.name).toBe('InvalidStateError')
+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 { jassubAssets } from '../helpers/jassub-assets.js'
import { skippableVast } from './vast-fixture.mjs'
const workspace = fileURLToPath(new URL('../../', import.meta.url))
@@ -28,6 +29,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-jassub')) {
for (const resource of jassubAssets(workspace, mapFile))
add(resource.route, resource.bytes, resource.source)
}
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
const route = `/candidate/${name}.js`
if (mapFile) {
+27
View File
@@ -0,0 +1,27 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { hash } from '../../refactor/scripts/releases.mjs'
import { verifyInstalledArtifacts } from '../../scripts/installed-artifacts.mjs'
export function jassubAssets(root, mapFile) {
const name = 'artplayer-plugin-jassub'
const baseline = JSON.parse(fs.readFileSync(path.join(root, 'refactor/baselines/jassub-release.json'), 'utf8'))
const installed = mapFile ? verifyInstalledArtifacts(root, mapFile, ['artplayer', 'artplayer-plugin-chapter', name]) : null
const input = installed?.inputs.find(item => item.name === name)
const pkg = installed?.packages.find(item => item.name === name)
return ['jassub-worker.js', 'jassub-worker.wasm', 'jassub-worker-modern.wasm', 'default.woff2'].map((filename) => {
const worker = filename !== 'default.woff2'
const relative = worker ? `packages/${name}/worker/${filename}` : `docs/assets/jassub/${filename}`
const expected = baseline.assets.find(item => item.file === relative)
assert(expected, `Missing JASSUB resource baseline: ${relative}`)
const member = `package/worker/${filename}`
const file = input && worker ? path.resolve(path.dirname(input.file), '../worker', filename) : path.join(root, relative)
const bytes = fs.readFileSync(file)
const sha256 = hash(bytes)
assert.equal(sha256, expected.sha256, `JASSUB resource differs from baseline: ${relative}`)
if (input && worker)
assert.equal(sha256, pkg.files[member], `Installed JASSUB resource differs from archive: ${member}`)
return { route: `/assets/jassub/${filename}`, bytes, source: { kind: input && worker ? 'installed-resource' : 'local-resource', file, sha256, baseline: relative, member: input && worker ? member : null, archiveSha256: input && worker ? input.archiveSha256 : null } }
})
}
+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']) {
for (const name of ['artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-plugin-jassub']) {
await assert.rejects(browserCandidate(name), { code: 'ENOENT' })
await assert.rejects(browserCandidate(name, 'override.js'), new RegExp(`cannot override ${name} artifact`))
}
+41 -2
View File
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
@@ -10,6 +11,7 @@ import { verifyInstalledArtifacts } from '../scripts/installed-artifacts.mjs'
import { verifyPerformanceArtifacts } from '../scripts/performance-artifacts.mjs'
import { performanceHtml, performanceScript, waitForObservation } from '../scripts/performance-fixture.mjs'
import { validatePairedPerformance } from '../scripts/performance-report.mjs'
import { jassubAssets } from './helpers/jassub-assets.js'
const baseline = JSON.parse(fs.readFileSync(new URL('../refactor/baselines/performance.json', import.meta.url), 'utf8')).runs[0]
function fixture() {
@@ -129,7 +131,7 @@ test('performance refuses stale sources, build tools and altered installed bundl
write(path.join(root, file), content)
write(path.join(snapshot, file), content)
}
const names = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight']
const names = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-plugin-jassub']
const packages = names.map((name) => {
const entry = `packages/${name}/src/index.ts`
write(path.join(root, entry), 'export default 1\n')
@@ -142,11 +144,48 @@ test('performance refuses stale sources, build tools and altered installed bundl
return { name, sha256: 'archive fixture', files: { [`package/dist/${name}.js`]: hash(bytes) } }
})
const map = path.join(output, 'browser-artifacts.json')
const jassub = packages.find(pkg => pkg.name === 'artplayer-plugin-jassub')
const assets = ['jassub-worker.js', 'jassub-worker.wasm', 'jassub-worker-modern.wasm', 'default.woff2'].map((name) => {
const worker = name !== 'default.woff2'
const file = worker ? `packages/artplayer-plugin-jassub/worker/${name}` : `docs/assets/jassub/${name}`
const bytes = Buffer.from(`fixture ${name}`)
write(path.join(root, file), bytes)
if (worker) {
write(path.join(snapshot, file), bytes)
write(path.join(output, `artifacts/artplayer-plugin-jassub/worker/${name}`), bytes)
jassub.files[`package/worker/${name}`] = hash(bytes)
}
return { file, sha256: hash(bytes) }
})
write(path.join(root, 'refactor/baselines/jassub-release.json'), JSON.stringify({ assets }))
write(map, JSON.stringify(Object.fromEntries(names.map(name => [name, `artifacts/${name}/dist/${name}.js`]))))
write(path.join(output, 'report.json'), JSON.stringify({ task: 'ENG-07', knownTypeBlockers: 0, node: process.versions.node, packages }))
assert.equal(verifyPerformanceArtifacts(root, map).inputs.length, 2)
assert.equal(verifyPerformanceArtifacts(root, map).packages.length, 2)
assert.equal(verifyInstalledArtifacts(root, map, names).inputs.length, 3)
assert.equal(verifyInstalledArtifacts(root, map, names).inputs.length, 4)
assert.deepEqual(jassubAssets(root, map).map(item => item.source.kind), ['installed-resource', 'installed-resource', 'installed-resource', 'local-resource'])
assert(jassubAssets(root).every(item => item.source.kind === 'local-resource'))
const worker = path.join(root, assets[0].file)
const originalWorker = fs.readFileSync(worker)
write(worker, 'stale worker')
assert.throws(() => jassubAssets(root, map), /Stale package source/)
write(worker, originalWorker)
const installedWorker = path.join(output, 'artifacts/artplayer-plugin-jassub/worker/jassub-worker.js')
write(installedWorker, 'tampered installed worker')
assert.throws(() => jassubAssets(root, map), /resource differs from baseline/)
write(installedWorker, originalWorker)
const reportFile = path.join(output, 'report.json')
const originalReport = fs.readFileSync(reportFile)
const missingMember = JSON.parse(originalReport)
delete missingMember.packages.find(pkg => pkg.name === 'artplayer-plugin-jassub').files['package/worker/jassub-worker.js']
write(reportFile, JSON.stringify(missingMember))
assert.throws(() => jassubAssets(root, map), /resource differs from archive/)
write(reportFile, originalReport)
const font = path.join(root, assets[3].file)
const originalFont = fs.readFileSync(font)
write(font, 'wrong font')
assert.throws(() => jassubAssets(root, map), /resource differs from baseline/)
write(font, originalFont)
assert.throws(() => verifyInstalledArtifacts(root, map, [...names, 'artplayer-proxy-canvas']), /Missing installed package/)
const pluginSource = path.join(root, 'packages/artplayer-plugin-ambilight/src/index.ts')
write(pluginSource, 'export default 2\n')