test(ci): [CI-01] centralize installed roster and cover ASR and Cast

This commit is contained in:
Harvey Zhao committed 2026-09-15 02:56:43 +08:00
1 parent b90de99bc2
commit 8379ac7bbf
30 files changed
+3028 -58

No files matched your search

+8 -7
View File
@@ -1,25 +1,26 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let asrCode
let audioCode
let inputs
test.beforeAll(async () => {
expect(process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'These source/explicit artifact checks do not represent isolated installed packages').toBeFalsy()
asrCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-asr', 'umd')
audioCode = process.env.ARTPLAYER_AUDIO_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-audio-track', 'umd')
const asr = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
asrCode = asr.code
const audio = await browserCandidate('artplayer-plugin-audio-track', process.env.ARTPLAYER_AUDIO_ARTIFACT)
audioCode = audio.code
const media = fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))
const manifest = JSON.parse(fs.readFileSync(new URL('./media/audio-tone.json', import.meta.url)))
expect(hash(media)).toBe(manifest.sha256)
inputs = {
asr: { input: process.env.ARTPLAYER_ASR_ARTIFACT || 'workspace source', sha256: hash(asrCode) },
audioTrack: { input: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source', sha256: hash(audioCode) },
asr: { provenance: asr.provenance, sha256: hash(asrCode) },
audioTrack: { provenance: audio.provenance, sha256: hash(audioCode) },
media: { file: 'test/browser/media/audio-tone.m4a', sha256: hash(media), bytes: media.length },
recognitionService: false,
scope: 'Native media/WebAudio composition; not physical speaker, device, or isolated package acceptance',
scope: 'Native media/WebAudio composition; not physical speaker or device acceptance',
}
})
+6 -3
View File
@@ -2,14 +2,17 @@ import fs from 'node:fs'
import process from 'node:process'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let contract
let candidateProvenance
let candidateCode
test.beforeAll(async () => {
contract = await verifyAsrContract()
candidateCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT) : await compilePackage('artplayer-plugin-asr', 'umd')
const candidate = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
candidateCode = candidate.code
candidateProvenance = candidate.provenance
})
for (const version of ['2.0.0', '2.1.0', 'candidate']) {
@@ -18,7 +21,7 @@ for (const version of ['2.0.0', '2.1.0', 'candidate']) {
const release = [contract.baseline.release, ...contract.baseline.previous].find(release => release.version === version)
const code = version === 'candidate' ? candidateCode : readMember(contract.archives.get(version), 'package/dist/artplayer-plugin-asr.js')
const media = fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))
await testInfo.attach('asr-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, pluginSha256: hash(code), integrity: release?.integrity || null, candidate: process.env.ARTPLAYER_ASR_ARTIFACT || 'workspace source', mediaSha256: hash(media), media: 'local mono AAC 440 Hz tone', networkService: false }) })
await testInfo.attach('asr-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, pluginSha256: hash(code), selected: version === 'candidate' ? candidateProvenance : { kind: 'published', version, sha256: hash(code) }, integrity: release?.integrity || null, mediaSha256: hash(media), media: 'local mono AAC 440 Hz tone', networkService: false }) })
await page.goto(`/test/player.html?core=${core}`)
const capabilities = await page.evaluate(() => ({ AudioContext: typeof window.AudioContext, webkitAudioContext: typeof window.webkitAudioContext, AudioWorkletNode: typeof window.AudioWorkletNode }))
await testInfo.attach('asr-audio-capabilities', { contentType: 'application/json', body: JSON.stringify(capabilities) })
+6 -4
View File
@@ -1,13 +1,15 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let provenance
let code
test.beforeAll(async () => {
expect(process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Explicit source/artifact tests do not represent installed packages').toBeFalsy()
code = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-asr', 'umd')
const candidate = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
code = candidate.code
provenance = candidate.provenance
})
test.afterEach(async ({ page }, testInfo) => {
@@ -33,7 +35,7 @@ for (const core of ['published', 'candidate']) {
})
await page.goto(`/test/player.html?core=${core}`)
const capabilities = await page.evaluate(() => ({ context: typeof (window.AudioContext || window.webkitAudioContext), worklet: typeof window.AudioWorkletNode, capture: typeof HTMLMediaElement.prototype.captureStream, mozCapture: typeof HTMLMediaElement.prototype.mozCaptureStream }))
await testInfo.attach('capture-cors-inputs', { contentType: 'application/json', body: JSON.stringify({ core, capabilities, plugin: process.env.ARTPLAYER_ASR_ARTIFACT || 'workspace source', pluginSha256: hash(code), mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), nativeCapture: true, forcedException: false, analyserBinding: false, physicalAudioOutput: false, scope: 'Zero direct bindings and native media clock progression preserve routing; no claim about physical speaker output' }) })
await testInfo.attach('capture-cors-inputs', { contentType: 'application/json', body: JSON.stringify({ core, capabilities, provenance, pluginSha256: hash(code), mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), nativeCapture: true, forcedException: false, analyserBinding: false, physicalAudioOutput: false, scope: 'Zero direct bindings and native media clock progression preserve routing; no claim about physical speaker output' }) })
if (capabilities.context === 'undefined') {
expect(process.platform).toBe('win32')
expect(testInfo.project.name).toBe('webkit')
+6 -3
View File
@@ -2,14 +2,17 @@ import fs from 'node:fs'
import process from 'node:process'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let contract
let candidateProvenance
let candidateCode
test.beforeAll(async () => {
contract = await verifyAsrContract()
candidateCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT) : await compilePackage('artplayer-plugin-asr', 'umd')
const candidate = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
candidateCode = candidate.code
candidateProvenance = candidate.provenance
})
for (const version of ['2.1.0', 'candidate']) {
@@ -26,7 +29,7 @@ for (const version of ['2.1.0', 'candidate']) {
test.skip(true, 'Windows WebKit lacks native WebAudio; this is not Safari CORS acceptance')
}
const code = version === 'candidate' ? candidateCode : readMember(contract.archives.get(version), 'package/dist/artplayer-plugin-asr.js')
await testInfo.attach('asr-cors-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, scenario, pluginSha256: hash(code), mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), physicalAudioOutput: false, securityBypass: false }) })
await testInfo.attach('asr-cors-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, scenario, pluginSha256: hash(code), selected: version === 'candidate' ? candidateProvenance : { kind: 'published', version, sha256: hash(code) }, mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), physicalAudioOutput: false, securityBypass: false }) })
const responses = []
page.on('response', (response) => {
if (/\/test\/(?:asr-cors\.m4a|audio-tone\.m4a|asr-opaque-redirect)(?:\?|$)/u.test(response.url()))
+6 -4
View File
@@ -1,15 +1,17 @@
import fs from 'node:fs'
import process from 'node:process'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let contract
let candidateProvenance
let candidateCode
test.beforeAll(async () => {
contract = await verifyAsrContract()
candidateCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT) : await compilePackage('artplayer-plugin-asr', 'umd')
const candidate = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
candidateCode = candidate.code
candidateProvenance = candidate.provenance
})
for (const { version, captureOnly } of [{ version: '2.1.0', captureOnly: false }, { version: 'candidate', captureOnly: false }, { version: 'candidate', captureOnly: true }]) {
@@ -19,7 +21,7 @@ for (const { version, captureOnly } of [{ version: '2.1.0', captureOnly: false }
await page.goto(`/test/player.html?core=${core}`)
const capabilities = await page.evaluate(() => ({ context: typeof AudioContext, worklet: typeof AudioWorkletNode, capture: typeof HTMLMediaElement.prototype.captureStream, mozCapture: typeof HTMLMediaElement.prototype.mozCaptureStream }))
const forceRejection = testInfo.project.name === 'firefox' && !captureOnly
await testInfo.attach('asr-fallback-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, captureOnly, pluginSha256: hash(code), capabilities, forcedException: forceRejection, reason: captureOnly ? 'Explicit capture mode never binds the element; no forced exceptions' : forceRejection ? 'Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet' : 'Native duplicate binding rejection', externalContext: 'Native media-element source remains owned and running', physicalOutput: false }) })
await testInfo.attach('asr-fallback-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, captureOnly, pluginSha256: hash(code), selected: version === 'candidate' ? candidateProvenance : { kind: 'published', version, sha256: hash(code) }, capabilities, forcedException: forceRejection, reason: captureOnly ? 'Explicit capture mode never binds the element; no forced exceptions' : forceRejection ? 'Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet' : 'Native duplicate binding rejection', externalContext: 'Native media-element source remains owned and running', physicalOutput: false }) })
if (capabilities.context !== 'function') {
expect(process.platform).toBe('win32')
expect(testInfo.project.name).toBe('webkit')
+6 -3
View File
@@ -2,14 +2,17 @@ import fs from 'node:fs'
import process from 'node:process'
import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs'
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let contract
let candidateProvenance
let candidateCode
test.beforeAll(async () => {
contract = await verifyAsrContract()
candidateCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT) : await compilePackage('artplayer-plugin-asr', 'umd')
const candidate = await browserCandidate('artplayer-plugin-asr', process.env.ARTPLAYER_ASR_ARTIFACT)
candidateCode = candidate.code
candidateProvenance = candidate.provenance
})
for (const version of ['2.1.0', 'candidate']) {
@@ -23,7 +26,7 @@ for (const version of ['2.1.0', 'candidate']) {
expect(testInfo.project.name).toBe('webkit')
test.skip(true, 'Windows WebKit lacks native AudioContext/AudioWorkletNode')
}
await testInfo.attach('asr-playback-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, pluginSha256: hash(code), mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), meter: 'Native unity analyser inserted between plugin gain and destination', physicalAudioOutput: false }) })
await testInfo.attach('asr-playback-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, pluginSha256: hash(code), selected: version === 'candidate' ? candidateProvenance : { kind: 'published', version, sha256: hash(code) }, mediaSha256: hash(fs.readFileSync(new URL('./media/audio-tone.m4a', import.meta.url))), meter: 'Native unity analyser inserted between plugin gain and destination', physicalAudioOutput: false }) })
await page.addScriptTag({ content: code.toString() })
await page.evaluate(() => {
const NativeContext = window.AudioContext
+6 -6
View File
@@ -1,14 +1,14 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { browserCandidate } from '../helpers/browser-candidate.js'
import { expect, test } from './fixtures.js'
let provenance
let candidate
test.beforeAll(async () => {
candidate = process.env.ARTPLAYER_CHROMECAST_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_CHROMECAST_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-chromecast', 'umd')
const input = await browserCandidate('artplayer-plugin-chromecast', process.env.ARTPLAYER_CHROMECAST_ARTIFACT)
candidate = input.code
provenance = input.provenance
})
async function setup(page, core, testInfo, loaded = true) {
@@ -112,7 +112,7 @@ async function setup(page, core, testInfo, loaded = true) {
}
}, loaded)
await expect.poll(() => page.evaluate(() => window.castPlayers.every(art => art.isReady))).toBe(true)
await testInfo.attach('chromecast-controlled-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, artifact: process.env.ARTPLAYER_CHROMECAST_ARTIFACT || 'candidate source', sha256: hash(candidate), sdk: 'controlled stub; no receiver, discovery, real SDK or Cast media playback acceptance', native: 'ArtPlayer core, DOM controls, browser click dispatch and local video' }) })
await testInfo.attach('chromecast-controlled-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, provenance, sha256: hash(candidate), sdk: 'controlled stub; no receiver, discovery, real SDK or Cast media playback acceptance', native: 'ArtPlayer core, DOM controls, browser click dispatch and local video' }) })
return external
}
+12 -2
View File
@@ -4,7 +4,8 @@ import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- This fixture exercises the repository's Node test runner.
import { test } from 'node:test'
import { checkFiles, checkPackages, publishedConsumer } from '../scripts/package-check.mjs'
import { installedPackages } from '../scripts/browser-validation/scope.ts'
import { checkFiles, checkPackages, packageOptions, publishedConsumer } from '../scripts/package-check.mjs'
import { removeConsumer, runtimeConsumer } from '../scripts/package-consumer.mjs'
import { emitterContracts } from './contracts/emitter.js'
import { ambilightCandidate } from './helpers/ambilight.js'
@@ -15,6 +16,15 @@ import { dpipCandidate } from './helpers/dpip.js'
import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js'
import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.js'
test('Package CLI keeps legacy defaults and prepares the same roster the browser requires', () => {
assert.deepEqual(packageOptions([]), { release: false, include: [] })
assert.deepEqual(packageOptions(['--release']), { release: true, include: [] })
assert.deepEqual(packageOptions(['--include=artplayer-plugin-asr']), { release: false, include: ['artplayer-plugin-asr'] })
assert.deepEqual(['artplayer', 'artplayer-plugin-chapter', ...packageOptions(['--browser']).include], installedPackages)
for (const args of [['--browser', '--release'], ['--browser', '--include=artplayer-plugin-asr'], ['--broser'], ['--include='], ['--include=x,x'], ['--include=x', '--include=y'], ['--browser', '--browser']])
assert.throws(() => packageOptions(args))
})
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/)
})
@@ -41,7 +51,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']) {
for (const name of ['artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast']) {
await assert.rejects(browserCandidate(name), { code: 'ENOENT' })
await assert.rejects(browserCandidate(name, 'override.js'), new RegExp(`cannot override ${name} artifact`))
}
+6 -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/Audio Track/VTT Thumbnail/Multiple Subtitles/HLS Control/DASH Control/Auto Thumbnail 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,artplayer-plugin-hls-control,artplayer-plugin-dash-control,artplayer-plugin-auto-thumbnail`.
default consumer scope is core and chapter. For the shared installed browser scope,
run `yarn test:package --browser`.
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
@@ -97,3 +97,7 @@ bundles or contradictory frozen-workspace flags fail instead of rebuilding sourc
removed historical files, then deliberately removes an actual published default
export and required entry in an isolated consumer to prove runtime failures are
detected. It is part of `yarn test:node`.
The --browser option follows the shared installed browser roster automatically;
do not copy a manual package list into CI. Explicit --include remains supported
for a deliberate subset. --browser cannot combine with --include or --release.