test(ci): [CI-01] checkpoint installed audio and subtitle coverage

This commit is contained in:
Harvey Zhao committed 2026-09-15 02:11:36 +08:00
1 parent 34e4136b43
commit 2d45135873
28 files changed
+5724 -37

No files matched your search

+1 -1
View File
@@ -140,7 +140,7 @@ jobs:
# These extra installed packages are required by the browser helpers.
# Other ecosystem installation/SDK gates remain tracked by CI-01.
run: |
yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads 2>&1 | tee refactor/.cache/ci/browser-packages.log
yarn test:package --include=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 2>&1 | tee refactor/.cache/ci/browser-packages.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Run all source browser checks
if: ${{ !cancelled() }}
@@ -89,3 +89,15 @@ The package .npmignore excludes the implementation tsconfig along with src. Yarn
retain all six declaration files and every historical dist/type path. Audio-04 verified these
files and identical JavaScript bytes to Audio-03; isolated installed consumers and the 8082
demo still belong to Audio-06. See refactor/baselines/audio-types-validation.json for evidence.
## Installed browser coverage
audio-fixture.js records the selected published or candidate input. The older test label "source audio" means the candidate slot; consult audio-inputs.selected for its actual origin. Native WAV capability controls remain separate from decoded AAC acceptance.
The shared `yarn test:browser:installed` roster includes this package after preparing
the nine-package tarballs with `yarn test:package --include=...`; use the exact command
in ../../scripts/browser-validation/README.md. Attachments identify candidate file,
archive and source hashes. Missing or stale maps fail without source fallback.
Without a map, source and deliberate artifact checks retain their existing behavior.
This is desktop main-entry coverage, not all module forms, physical devices or
release readiness. See ../../refactor/changes/2026-09-15-CI-01-subtitles-installed.md.
@@ -183,3 +183,15 @@ See task09's change/validation record and the core offset change for precise sco
The matrix is runnable with `yarn test:browser test/browser/multiple-subtitles-combinations.spec.js`;
do not replace these failures with skips or count boundary observations as acceptance.
See `../../refactor/baselines/multiple-subtitles-contract.md` for exact historical differences.
## Installed browser coverage
multipleSubtitlesCandidate uses browser-candidate.js for verified installed bytes. An installed map rejects ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT overrides. Actual historical plugins and no-plugin controls retain their original inputs.
The shared `yarn test:browser:installed` roster includes this package after preparing
the nine-package tarballs with `yarn test:package --include=...`; use the exact command
in ../../scripts/browser-validation/README.md. Attachments identify candidate file,
archive and source hashes. Missing or stale maps fail without source fallback.
Without a map, source and deliberate artifact checks retain their existing behavior.
This is desktop main-entry coverage, not all module forms, physical devices or
release readiness. See ../../refactor/changes/2026-09-15-CI-01-subtitles-installed.md.
@@ -159,3 +159,15 @@ still target source and must not be described as tests of an installed tarball.
See ../../refactor/changes/2026-09-13-PKG-VTT-THUMB-04-module-forms.md,
../../refactor/baselines/vtt-thumbnail-contract.md and the task 03 change record for
historical evidence and outstanding acceptance work.
## Installed browser coverage
vttThumbnailCandidate uses browser-candidate.js for verified installed bytes. An installed map rejects ARTPLAYER_VTT_THUMBNAIL_BASELINE and ARTPLAYER_VTT_THUMBNAIL_ARTIFACT overrides. Frozen published control inputs and their cue-format differences remain explicit.
The shared `yarn test:browser:installed` roster includes this package after preparing
the nine-package tarballs with `yarn test:package --include=...`; use the exact command
in ../../scripts/browser-validation/README.md. Attachments identify candidate file,
archive and source hashes. Missing or stale maps fail without source fallback.
Without a map, source and deliberate artifact checks retain their existing behavior.
This is desktop main-entry coverage, not all module forms, physical devices or
release readiness. See ../../refactor/changes/2026-09-15-CI-01-subtitles-installed.md.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,60 @@
# CI-01 Audio Track、VTT Thumbnail 与 Multiple Subtitles 安装验证
## 实现与兼容边界
统一 installed 名单从六包扩展为九包,新增三个插件的十二个浏览器文件。
package-check 先校验各自冻结发布契约,再构建、打包、独立安装、冻结复装和核对
完整安装文件。工作流准备命令与静态校验同步;通用运行时/类型消费者仍只负责
core/chapter,不把新增浏览器覆盖当作全包声明或发布准入。
三个候选加载器使用 browser-candidate.js,安装 map 存在时读取已验证的 tarball
安装文件,禁止显式 artifact 覆盖;VTT 也拒绝 frozen-workspace baseline 标记。
缺失、篡改、过期安装不能回退源码。无 map 时保留源码和显式产物方式。
附件记录实际候选文件、归档与来源指纹;Audio 的旧标题 source audio 是候选槽位,
实际来源查 audio-inputs.selected。真实旧插件和 no-plugin 控制继续保留原输入。
本轮只修改测试、工程入口与维护文档,没有修改生产 API、播放器实现、超时、
依赖或锁文件。一处 ASS 等待断言修正见下文,最终数量/文本要求保持严格。
VAST 类型决定已有独立实施,不合并到本 CI 检查点。
## 验证范围
Windows x64,Node 24.21.0、Yarn 1.22.22。九包安装 run-QsLTUc 通过;通用
core/chapter 36 运行时检查、5/5 旧类型模式、8/8 精确类型模式通过。
新增插件相关单元 504/504、工程 46/46、scoped lint、library 类型、严格工具链、
CI 工作流契约与 actionlint 1.7.12 通过;actionlint 未运行 shellcheck/pyflakes。
完整 installed collection 为 25 文件、762 项。实际执行仅新增十二文件、三个
引擎的 336 项,workers=2、retries=0;最终结果与原始报告指纹见机器证据。
这不是本次运行完整九包套件;此前 CHAPTER-TIMING-01 失败继续保留。WAV能力、
旧字幕宿主缺陷及合成移动输入等控制只证明其记录的范围,不能冒充真机通过。
各包 ARCHITECTURE.md 与 scripts/browser-validation/README.md 记录加载边界。
CI-01 继续 doing,远端 Actions、其他生态包及最终设备/发布门槛未完成。
撤销本检查点可恢复六包 CI 范围;不涉及生产源码回退。未推送或发布。
## 实测结果与修复
首轮 336 项为 327 通过、9 失败,耗时 388.566 秒,实际退出 1。Chromium 和
Firefox 各 112/112 通过;WebKit 103/112。失败中的八项是已有 AUDIO-BUFFER-01
的新旧音频组合;七项未等到可信 waiting 且时间大于 0.3 的目标状态,一项在受限
视频路径耗尽测试时间并在 finally 的状态采集中报页面关闭。保留真实等待和失败,
不把销毁后的 error=4 当作加载阶段解码失败;未证明新的底层根因。
另一项是旧核心 published + 候选 Multiple Subtitles 的 ASS 断言:seeked 后,
单元素文本 locator 在旧字幕三个节点尚未替换时立即 strict-mode 失败。失败快照
已经显示 Later cue,视频时间 5.002334;不是字幕永久丢失的证据。将断言改为
exact array [Later cue],在原 7000ms 内同时要求恰好一个元素与准确文本;不接受
额外节点、子串或错误字幕。增加检查前的产物附件,失败也可识别输入。修正后
五核心 × 三引擎 ASS 15/15 通过,没有重试;原始九失败报告不被覆盖。
三个加载器的 Chromium 源码定向实际媒体测试 3/3,通过继承安装 map 的 source
入口执行,附件均明确 source-build。只证明这三个开发路径继续可用。
初轮有 197 项附件直接验证候选安装身份;失败的 ASS 在原断言之前尚无其最终
附件,不能把缺失附件计入该数;修正后的 15 项均在操作前验证安装身份。
机器证据见 [ci-subtitles-installed-validation.json](../baselines/ci-subtitles-installed-validation.json)。
原日志和报告位于 ci01-subtitles-installed-report/source-report/ass-report,各自保持
实际退出状态。安装报告内的旧附件路径已随目录归档,读取时用归档目录下 results。
完整九包套件未在本次重跑,八项音频失败仍开放。准备 tarball 后补充了包内架构
说明,本轮不是最终发布候选批准;发布前仍需重新打包绑定最终内容。
+1 -1
View File
@@ -2,7 +2,7 @@
浏览器CI现在分别执行 `yarn test:browser:source` 和
`yarn test:browser:installed`。前者清除继承的安装map并保留全部spec;后者要求已
核验的core/chapter/Ambilight/Canvas/Document PiP/Ads安装,执行明确列出的installed子集。两步不
核验的core/chapter/Ambilight/Canvas/Document PiP/Ads/Audio Track/VTT Thumbnail/Multiple Subtitles安装,执行明确列出的installed子集。两步不
吞失败,普通失败后仍保存另一范围的证据,分别上传browser-source/browser-installed。
入口与报告规则见[维护说明](../scripts/browser-validation/README.md)。source全量
耗时/60分钟预算、其余插件安装矩阵与远端运行仍待CI-01/CI-04,不计为已通过。
+1 -1
View File
@@ -508,7 +508,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)
- 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)
- 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)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## CI-01 音频、缩略图和多字幕接入安装矩阵
九包安装与完整文件核验通过,统一安装清单扩展到 25 文件/762 项。本轮新增
十二文件三引擎实跑 336 项:327 通过、9 失败。八项 Windows WebKit 音频断流
保持 AUDIO-BUFFER-01;ASS 的过早单元素断言已修正,随后全部 15 组合通过,
原失败报告保留。源码实际媒体 3/3、相关单元 504/504、工程 46/46 通过。
见[记录](changes/2026-09-15-CI-01-subtitles-installed.md)。CI-01 继续 doing,
完成数仍 196/262;下一步接续剩余包安装覆盖和真实环境门槛,未声称全量 CI 通过。
## PKG-CHAPTER-05 原生时钟对照检查点
默认 trace 与关闭 trace 的两组固定 WebKit 安装组合均为 11/12 通过,保留失败。
+5 -2
View File
@@ -2877,10 +2877,13 @@
"test/helpers/media-gate.js",
"test/media-gate.test.js",
"refactor/baselines/audio-combinations-checkpoint.json",
"refactor/audio-validation.md"
"refactor/audio-validation.md",
"refactor/changes/2026-09-15-CI-01-subtitles-installed.md",
"refactor/baselines/ci-subtitles-installed-validation.json"
],
"compatibleResolution": "保留真实 waiting 断言;使用独立原生对照和其他实际支持环境/输入继续验证,不将没有播放推进的诊断当成缓冲通过,也不凭此改变插件 API 或否定真实 Safari 支持。",
"closureCriteria": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。"
"closureCriteria": "受影响的缓冲/恢复得到真实媒体证据;与原生能力和测试夹具差异分开,发布环境的剩余范围经显式处理。",
"workspaceState": "CI-01 九包安装扩展的首轮 336 项中,Windows WebKit 八项新旧音频受限媒体组合继续失败。七项未观察到目标可信 waiting,一项受限视频路径耗尽整体时间并在最终状态读取中报页面关闭。其他引擎均通过;保留原始失败,不以销毁后状态反推初始解码错误,不推断新的共同底层原因。"
},
{
"id": "CHAPTER-LAYOUT-01",
+1 -1
View File
@@ -90,7 +90,7 @@ export function validateCIWorkflow(source) {
const vueIndex = browser.steps.findIndex(step => step.run === 'yarn test:vue-consumer 2>&1 | tee refactor/.cache/ci/vue-consumer.log')
assert(vueIndex > consumerIndex && !Object.hasOwn(browser.steps[vueIndex], 'if'), 'Run Vue installed consumers after restoring canonical Node')
assert(browser.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') && step.if === 'always()' && step.with.path.split('\n').includes('refactor/.cache/vue-consumer-*/')), 'Retain Vue consumer failure evidence')
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads '))
const extraIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package --include=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 '))
const sourceEngineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:source 2>&1 | tee refactor/.cache/ci/browser-source.log')
const engineIndex = browser.steps.findIndex(step => step.run === 'yarn test:browser:installed 2>&1 | tee refactor/.cache/ci/browser-installed.log')
assert(extraIndex > Math.max(consumerIndex, reactIndex, vueIndex) && extraIndex < engineIndex, 'Prepare installed Ambilight/Canvas/Document PiP/Ads after consumer probes and before browser checks')
+3 -1
View File
@@ -764,7 +764,9 @@
"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"
"baselines/ci-ads-installed-validation.json",
"changes/2026-09-15-CI-01-subtitles-installed.md",
"baselines/ci-subtitles-installed-validation.json"
]
},
{
+13 -2
View File
@@ -14,8 +14,8 @@ also retains explicit historical and committed-artifact cases; its name does not
promise that every dependency is rebuilt from current source in every test.
`yarn test:browser:installed` requires ARTPLAYER_BROWSER_ARTIFACTS from
`yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads`.
Both launcher and config validate the selected six packages' installed hashes
`yarn test:package --include=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`.
Both launcher and config validate the selected nine packages' installed hashes
and source/build freshness. `scope.ts` lists the currently supported installed
test files. This is a growing installed subset, not full ecosystem acceptance.
Source checks remain complete when the installed list grows.
@@ -63,3 +63,14 @@ keeps the job failed. Both report directories upload with always(). Node/runtime
and framework consumers keep their earlier fixed two-package scope. Full matrix
timing, remote cancellation behavior and runner execution remain CI-01/CI-04
acceptance items; local configuration checks do not establish those results.
The Audio Track, VTT Thumbnail and Multiple Subtitles expansion adds 12 files.
Each candidate loader selects verified installed bytes when a map is present and
rejects a conflicting explicit artifact; VTT also rejects its frozen baseline flag.
Candidate attachments carry the file, archive and source identities. Audio retains
its historical "source audio" test label for the candidate slot; audio-inputs.selected
is the actual origin. Native WAV capability and old-host defect observations must
not be counted as successful feature acceptance. Shared Node/type consumers retain
the core/chapter scope. See ../../refactor/changes/2026-09-15-CI-01-subtitles-installed.md
for the executed subset and results; extending collection does not revalidate the
previous Chapter failure or establish a green complete matrix.
+13 -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']
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']
export const installedTests = [
'playback.spec.js',
'lifecycle.spec.js',
@@ -17,6 +17,18 @@ export const installedTests = [
'canvas-dpip.spec.js',
'ads.spec.js',
'ads-ui.spec.js',
'audio-track.spec.js',
'audio-combinations.spec.js',
'audio-buffering.spec.js',
'vtt-thumbnail-lifecycle.spec.js',
'vtt-thumbnail-combinations.spec.js',
'multiple-subtitles-lifecycle.spec.js',
'multiple-subtitles-legacy.spec.js',
'multiple-subtitles-history.spec.js',
'multiple-subtitles-entities.spec.js',
'multiple-subtitles-combinations.spec.js',
'multiple-subtitles-ass.spec.js',
'multiple-subtitles-switch.spec.js',
]
export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
+3
View File
@@ -77,6 +77,9 @@ export async function checkPackages({ release = false, include = [] } = {}) {
'artplayer-proxy-canvas': async () => (await import('../refactor/scripts/canvas-contract.mjs')).verifyCanvasContract(),
'artplayer-plugin-document-pip': async () => (await import('../refactor/scripts/dpip-contract.mjs')).verifyDpipContract(),
'artplayer-plugin-ads': async () => (await import('../refactor/scripts/ads-contract.mjs')).verifyAdsContract(),
'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(),
}
assert(new Set(include).size === include.length, 'Duplicate additional package')
const historicalFiles = new Map()
+5 -5
View File
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
export function useAudioFixture() {
@@ -32,14 +32,14 @@ export function useAudioFixture() {
test.beforeAll(async () => {
assert.equal(hash(tone), toneManifest.sha256)
assert.equal(tone.length, toneManifest.bytes)
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Audio source tests cannot represent installed artifacts')
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/audio-track-release.json', import.meta.url)))
const member = 'package/dist/artplayer-plugin-audio-track.js'
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
publishedCode = bytes.toString()
sourceCode = process.env.ARTPLAYER_AUDIO_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
evidence = { release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source build', audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Source or explicit artifact browser checks; no physical device or installed package acceptance' }
const candidate = await browserCandidate(release.name, process.env.ARTPLAYER_AUDIO_ARTIFACT)
sourceCode = candidate.code
evidence = { release, sourceSHA256: hash(sourceCode), candidate: candidate.provenance, audio: { sha256: hash(tone), bytes: tone.length, format: 'AAC mono 48000 Hz, 16 seconds, 440 Hz tone in MP4' }, wav: { sha256: hash(wav), bytes: wav.length, format: 'PCM16LE mono 16000 Hz, 16 seconds, integer square wave 200 Hz' }, scope: 'Desktop browser media checks; candidate provenance identifies installed, source or explicit artifact. No physical device acceptance.' }
})
test.afterEach(async ({ page }, testInfo) => {
@@ -53,7 +53,7 @@ export function useAudioFixture() {
})
async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m4a?source=first') {
await testInfo.attach('audio-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
await testInfo.attach('audio-inputs', { contentType: 'application/json', body: JSON.stringify({ ...evidence, core, plugin, selected: plugin === 'published' ? { kind: 'published', sha256: hash(publishedCode) } : evidence.candidate }) })
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((url) => {
+3 -2
View File
@@ -8,6 +8,7 @@ const ass = '[Events]\r\nDialogue: 0,0:00:01.25,0:00:03.75,Default,,0,0,0,,漢
for (const core of ['published-5.1.2', 'published-5.1.7', 'published-5.3.0', 'published', 'candidate']) {
test(`Multiple subtitles ASS / ${core}: encoded multiline cues, overlapping translation and selection`, async ({ page, browser }, testInfo) => {
await testInfo.attach('ass-inputs', { contentType: 'application/json', body: JSON.stringify({ core, sha256: hash(candidate.code), provenance: candidate.provenance }) })
await page.route('**/test/ass-data.bin', route => route.fulfill({ contentType: 'application/octet-stream', body: Buffer.from(ass, 'utf16le') }))
await page.route('**/test/ass-translation.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:02.000 --> 00:04.000\nTranslation\n' }))
await page.goto(`/test/player.html?core=${core}`)
@@ -47,12 +48,12 @@ for (const core of ['published-5.1.2', 'published-5.1.7', 'published-5.3.0', 'pu
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(page.locator('.art-subtitle-ass b')).toBeVisible()
await seek(5)
await expect(page.locator('.art-subtitle-ass')).toHaveText('Later cue')
await expect(page.locator('.art-subtitle-ass')).toHaveText(['Later cue'])
await expect(page.locator('.art-subtitle-translation')).toHaveCount(0)
await seek(7.5)
await expect(page.locator('.art-subtitle-ass, .art-subtitle-translation')).toHaveCount(0)
const rawHostOutput = await page.evaluate(ass => window.Artplayer.utils.assToVtt(ass), ass)
await testInfo.attach('ass-conversion', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), sourceSha256: hash(Buffer.from(ass, 'utf16le')), rawHostOutput, native, scope: 'Desktop native playback with UTF16 ASS, explicit type override, independently timed multiline/semantic captions, overlap, selection/reset and clearing; original core converter is unchanged.' }) })
await testInfo.attach('ass-conversion', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), provenance: candidate.provenance, sourceSha256: hash(Buffer.from(ass, 'utf16le')), rawHostOutput, native, scope: 'Desktop native playback with UTF16 ASS, explicit type override, independently timed multiline/semantic captions, overlap, selection/reset and clearing; original core converter is unchanged.' }) })
await page.evaluate(() => window.art.destroy())
})
}
@@ -32,7 +32,7 @@ async function prepare(page, core, implementation, browser, testInfo) {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
if (['1', 'events'].includes(process.env.ARTPLAYER_SOURCE_SEEK_TRACE))
await installSeekDiagnostics(page, process.env.ARTPLAYER_SOURCE_SEEK_TRACE === '1')
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), browser: browser.version(), traceMode: process.env.ARTPLAYER_SOURCE_SEEK_TRACE || 'off', sourceRestoration: process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1' ? 'native-event-control' : 'promise-and-seeking-property', scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
await testInfo.attach('multiple-combination-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, pluginSha256: hash(implementation.code), provenance: implementation.provenance, browser: browser.version(), traceMode: process.env.ARTPLAYER_SOURCE_SEEK_TRACE || 'off', sourceRestoration: process.env.ARTPLAYER_SOURCE_RESTORE_EVENT === '1' ? 'native-event-control' : 'promise-and-seeking-property', scope: 'Desktop engines; 5.3.0 is adjacent stable, not the unavailable 5.3.1 release; no minimum-version or physical-device claim' }) })
}
async function seek(page, time) {
@@ -35,7 +35,7 @@ for (const core of ['published', 'candidate']) {
await expect.poll(() => page.locator('.art-subtitle-a').textContent()).toBe(literal)
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
await expect(page.locator('.art-subtitle-b b')).toHaveText('bold & safe')
await testInfo.attach('entities.json', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(implementation.code), native, literal, scope: 'Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only' }) })
await testInfo.attach('entities.json', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(implementation.code), provenance: implementation.provenance, native, literal, scope: 'Actual native VTT and HTML caption display, playback, selection/reset; desktop browser engines only' }) })
await page.evaluate(() => window.art.destroy(false))
expect(errors).toEqual([])
})
@@ -69,7 +69,7 @@ for (const core of ['published-5.1.2', 'published-5.1.7']) {
await expect(page.locator('.art-subtitle-c')).toHaveText('C')
await seek(4.5)
await expect(page.locator('.art-subtitle-a, .art-subtitle-b, .art-subtitle-c')).toHaveCount(0)
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), thirdFormat, state, scope: 'Native VTT/SRT/ASS overlaps on both old cores, semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals' }) })
await testInfo.attach('legacy-captions', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), pluginSha256: hash(candidate.code), provenance: candidate.provenance, thirdFormat, state, scope: 'Native VTT/SRT/ASS overlaps on both old cores, semantic HTML/CSS, legacy scalar event payload, original host method and cue identities, repeated update/selection/reset/empty intervals' }) })
await page.evaluate(() => window.art.destroy())
})
}
@@ -40,7 +40,7 @@ async function prepare(page, core) {
async function evidence(page, testInfo, core, observation) {
const probe = await page.evaluate(() => ({ ...window.multipleProbe, signal: window.multipleProbe.signal ? { aborted: window.multipleProbe.signal.aborted } : null }))
expect(probe.errors).toEqual([])
await testInfo.attach('multiple-subtitles-lifecycle', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code) }, observation, probe }), contentType: 'application/json' })
await testInfo.attach('multiple-subtitles-lifecycle', { body: JSON.stringify({ core, implementation: { name: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance }, observation, probe }), contentType: 'application/json' })
}
for (const core of ['published', 'candidate']) {
@@ -75,7 +75,7 @@ for (const core of ['published-5.3.0', 'published', 'candidate']) {
await expect(page.locator('.art-subtitle-caption')).toHaveText('Caption')
outcomes.push({ round, settled, firstSeek, landed, afterSettledSeek: await page.evaluate(() => window.switchState()) })
}
await testInfo.attach('source-seek-order', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation?.name || 'none', pluginSha256: implementation ? hash(implementation.code) : null, browser: browser.version(), outcomes, trace: await page.evaluate(() => window.switchTrace), scope: 'Three real source changes with immediate versus settled seek; old misses are recorded, candidate must honor every first seek' }) })
await testInfo.attach('source-seek-order', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation?.name || 'none', pluginSha256: implementation ? hash(implementation.code) : null, provenance: implementation?.provenance, browser: browser.version(), outcomes, trace: await page.evaluate(() => window.switchTrace), scope: 'Three real source changes with immediate versus settled seek; old misses are recorded, candidate must honor every first seek' }) })
if (core === 'candidate')
expect(outcomes.every(item => item.landed)).toBe(true)
await page.evaluate(() => window.art.destroy())
@@ -32,7 +32,7 @@ async function install(page, core, implementation, testInfo) {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await testInfo.attach('vtt-combination-inputs', {
contentType: 'application/json',
body: JSON.stringify({ core, implementation: implementation.name, sha256: hash(implementation.code), cueFormat: /^published-1\.0\./.test(implementation.name) ? 'historical compact arrow; ordinary-space failures retained separately' : 'standard spaced arrow', mobile: 'desktop unless test declares an emulated user agent; no physical-device inference' }),
body: JSON.stringify({ core, implementation: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance, cueFormat: /^published-1\.0\./.test(implementation.name) ? 'historical compact arrow; ordinary-space failures retained separately' : 'standard spaced arrow', mobile: 'desktop unless test declares an emulated user agent; no physical-device inference' }),
})
return () => requests
}
@@ -157,7 +157,7 @@ for (const core of ['published-5.1.7', 'published', 'candidate']) {
})
expect(state.error).toContain('Can\'t add an existing [thumbnails] to the [control]')
expect(state.sameReservedControl).toBe(true)
await testInfo.attach('vtt-historical-conflict', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, sha256: hash(implementation.code), state, scope: 'Existing incompatibility, not successful old-plugin/new-core acceptance and not a newly introduced refactor regression' }) })
await testInfo.attach('vtt-historical-conflict', { contentType: 'application/json', body: JSON.stringify({ core, plugin: implementation.name, sha256: hash(implementation.code), provenance: implementation.provenance, state, scope: 'Existing incompatibility, not successful old-plugin/new-core acceptance and not a newly introduced refactor regression' }) })
})
}
+3 -3
View File
@@ -43,7 +43,7 @@ for (const core of ['published', 'candidate']) {
await testInfo.attach('selected-sprite', { body: await preview.screenshot(), contentType: 'image/png' })
await page.evaluate(() => window.art.destroy(false))
await expect(preview).toHaveCount(0)
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), pixels }), contentType: 'application/json' })
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), provenance: implementation.provenance, pixels }), contentType: 'application/json' })
})
test(`VTT native ${core} core: malformed sprite rejects before mounting and a corrected registration recovers`, async ({ page }, testInfo) => {
@@ -69,7 +69,7 @@ for (const core of ['published', 'candidate']) {
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(1)
await page.evaluate(() => window.art.destroy(false))
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0)
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), recoveredAfter: error }), contentType: 'application/json' })
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), provenance: implementation.provenance, recoveredAfter: error }), contentType: 'application/json' })
})
test(`VTT native ${core} core: destroy aborts a pending native fetch and settles registration`, async ({ page }, testInfo) => {
@@ -111,7 +111,7 @@ for (const core of ['published', 'candidate']) {
await page.evaluate(() => window.vttPending)
await expect(page.locator('.art-control-vtt-thumbnail')).toHaveCount(0)
expect(await page.evaluate(() => window.vttResult)).toEqual({ name: 'artplayerPluginVttThumbnail' })
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), nativeAbort: true }), contentType: 'application/json' })
await testInfo.attach('vtt-candidate', { body: JSON.stringify({ core, implementation: hash(implementation.code), provenance: implementation.provenance, nativeAbort: true }), contentType: 'application/json' })
}
finally { release() }
})
+2 -3
View File
@@ -1,15 +1,14 @@
import fs from 'node:fs'
import process from 'node:process'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyMultipleSubtitlesContract } from '../../refactor/scripts/multiple-subtitles-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from './load.js'
import { browserCandidate } from './browser-candidate.js'
export const subtitleVtt = text => `WEBVTT\n\n00:00.000 --> 00:02.000\n${text}\n`
export async function multipleSubtitlesCandidate() {
return { name: 'candidate', version: 'candidate', code: process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-multiple-subtitles', 'umd') }
return { name: 'candidate', version: 'candidate', ...await browserCandidate('artplayer-plugin-multiple-subtitles', process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT) }
}
export async function multipleSubtitlesHistorical() {
+6 -4
View File
@@ -1,15 +1,17 @@
import fs from 'node:fs'
import assert from 'node:assert/strict'
import process from 'node:process'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { verifyVttThumbnailContract } from '../../refactor/scripts/vtt-thumbnail-contract.mjs'
import { compilePackage } from './load.js'
import { browserCandidate } from './browser-candidate.js'
export async function vttThumbnailCandidate() {
if (process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE === '1')
if (process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE === '1') {
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed VTT thumbnail checks cannot use the frozen workspace')
return (await vttThumbnailHistorical()).find(item => item.name === 'frozen-workspace-js')
return { name: 'candidate', profile: 'current', code: process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-vtt-thumbnail', 'umd') }
}
return { name: 'candidate', profile: 'current', ...await browserCandidate('artplayer-plugin-vtt-thumbnail', process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT) }
}
export async function vttThumbnailHistorical() {
+17 -1
View File
@@ -11,13 +11,15 @@ import { ambilightCandidate } from './helpers/ambilight.js'
import { browserCandidate } from './helpers/browser-candidate.js'
import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js'
import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js'
import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.js'
test('Additional browser packages cannot be mistaken for full release consumer acceptance', async () => {
await assert.rejects(checkPackages({ release: true, include: ['artplayer-plugin-ambilight'] }), /not full release acceptance/)
})
test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => {
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT']
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT', 'ARTPLAYER_VTT_THUMBNAIL_BASELINE', 'ARTPLAYER_VTT_THUMBNAIL_ARTIFACT', 'ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT']
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json')
@@ -26,11 +28,25 @@ test('Explicit installed plugin maps never fall back to source or frozen workspa
delete process.env.ARTPLAYER_CANVAS_BASELINE
delete process.env.ARTPLAYER_DPIP_BASELINE
delete process.env.ARTPLAYER_DPIP_ARTIFACT
delete process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE
delete process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT
delete process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT
await assert.rejects(ambilightCandidate(), { code: 'ENOENT' })
await assert.rejects(canvasCandidate(), { code: 'ENOENT' })
await assert.rejects(dpipCandidate(), { code: 'ENOENT' })
await assert.rejects(browserCandidate('artplayer-plugin-ads'), { code: 'ENOENT' })
await assert.rejects(browserCandidate('artplayer-plugin-ads', 'override.js'), /cannot override artplayer-plugin-ads artifact/)
await assert.rejects(browserCandidate('artplayer-plugin-audio-track'), { code: 'ENOENT' })
await assert.rejects(browserCandidate('artplayer-plugin-audio-track', 'override.js'), /cannot override artplayer-plugin-audio-track artifact/)
await assert.rejects(vttThumbnailCandidate(), { code: 'ENOENT' })
await assert.rejects(multipleSubtitlesCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE = '1'
await assert.rejects(vttThumbnailCandidate(), /cannot use the frozen workspace/)
delete process.env.ARTPLAYER_VTT_THUMBNAIL_BASELINE
process.env.ARTPLAYER_VTT_THUMBNAIL_ARTIFACT = 'override.js'
process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT = 'override.js'
await assert.rejects(vttThumbnailCandidate(), /cannot override artplayer-plugin-vtt-thumbnail artifact/)
await assert.rejects(multipleSubtitlesCandidate(), /cannot override artplayer-plugin-multiple-subtitles artifact/)
process.env.ARTPLAYER_AMBILIGHT_BASELINE = '1'
process.env.ARTPLAYER_CANVAS_BASELINE = '1'
process.env.ARTPLAYER_DPIP_BASELINE = '1'
+2 -2
View File
@@ -9,8 +9,8 @@ step through `ARTPLAYER_BROWSER_ARTIFACTS`. See
browser scope and the remaining package-specific release gates.
Run `yarn test:package` with the pinned Node and Yarn after a frozen install. The
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP/Ads browser work,
run `yarn test:package --include=artplayer-plugin-ambilight,artplayer-proxy-canvas,artplayer-plugin-document-pip,artplayer-plugin-ads`.
default consumer scope is core and chapter. For Ambilight/Canvas/Document PiP/Ads/Audio Track/VTT Thumbnail/Multiple Subtitles browser work,
run `yarn test:package --include=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`.
These four additional packages have reviewed published contracts and use the same
source snapshot/build/pack/offline-install/frozen-reinstall pipeline. Installed
files are copied only after archive hash checks. The report explicitly separates