mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(ci): [CI-01] verify installed JASSUB bundles and worker resources
This commit is contained in:
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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } }
|
||||
})
|
||||
}
|
||||
@@ -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`))
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user