mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 00:06:17 -08:00
test: ENG-03 share behavior fixtures across source and releases
This commit is contained in:
1 parent
b53461da1c
commit
458bc3215d
15 files changed
+260
-74
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# Tests and fixture ownership
|
||||
|
||||
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `yarn test:unit` | Playback and DASH regressions, public contracts against released/current code, and JS/TS fixture loading |
|
||||
| `yarn test:node` | Unit contracts plus toolchain and documentation build failure propagation |
|
||||
| `yarn test` | Node checks and the committed baseline/tooling tests in refactor/scripts |
|
||||
| `yarn ci:check` | Toolchain, plan, read-only lint, types, then yarn test |
|
||||
| `yarn test:imports` | Existing distribution import smoke examples; run after building |
|
||||
|
||||
`helpers/load.js` owns source and artifact selection. `loadModules` bundles named internal modules with esbuild and requires exactly one JS/TS source file. `loadPackage` uses the repository Vite configuration without writing distribution files; it requires a single self-contained JavaScript chunk. These loaders transpile but do not replace `yarn typecheck`.
|
||||
|
||||
`helpers/playback.js` creates the controlled media facade used by the original playback regressions. It models only the exercised methods and events. It is not HTMLMediaElement validation; actual playback remains in browser tests.
|
||||
|
||||
`contracts/emitter.js` owns public assertions, independent of module layout. `public-behavior.test.js` runs them against the integrity-checked published core and current bundled source. The combined chain/context contract corresponds to BASE-03 EVENT.chain and EVENT.context-arguments; other EVENT IDs retain their baseline names. Exceptions, callback identity, mutation during dispatch and once reentry must not be weakened during migration.
|
||||
|
||||
Set `ARTPLAYER_TEST_CORE` to a built `.js`, `.legacy.js` or `.mjs` core file to add a candidate to the same public-contract run, then execute `node --test test/public-behavior.test.js`. An invalid path/export fails the run. This is a narrow event contract check, not isolated package installation or complete API compatibility; ENG-07 owns tarball consumption.
|
||||
|
||||
When a production module moves, update its loader mapping and its maintenance documentation, preserving the behavioral assertions. New contracts belong in contracts/; test-specific controlled state belongs in helpers/. Do not modify frozen refactor/fixtures or baseline captures just to pass changed behavior. Record historical defects and candidate fixes separately.
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
export const emitterContracts = {
|
||||
'EVENT.chain-context-arguments': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const context = {}
|
||||
const payload = {}
|
||||
let observed
|
||||
assert.equal(emitter.on('value', function (...args) {
|
||||
observed = { context: this, args }
|
||||
}, context), emitter)
|
||||
assert.equal(emitter.emit('value', payload, 42), emitter)
|
||||
assert.equal(observed.context, context)
|
||||
assert.deepEqual(observed.args, [payload, 42])
|
||||
assert.equal(emitter.off('value'), emitter)
|
||||
},
|
||||
'EVENT.once-reentry': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
let calls = 0
|
||||
emitter.once('value', () => {
|
||||
calls++
|
||||
emitter.emit('value')
|
||||
})
|
||||
emitter.emit('value').emit('value')
|
||||
assert.equal(calls, 1)
|
||||
},
|
||||
'EVENT.dispatch-snapshot': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const trace = []
|
||||
const second = () => trace.push('second')
|
||||
const later = () => trace.push('later')
|
||||
emitter.on('value', () => {
|
||||
trace.push('first')
|
||||
emitter.off('value', second)
|
||||
emitter.on('value', later)
|
||||
}).on('value', second)
|
||||
emitter.emit('value').emit('value')
|
||||
assert.deepEqual(trace, ['first', 'second', 'first', 'later'])
|
||||
},
|
||||
'EVENT.off-original-once': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
let calls = 0
|
||||
const listener = () => calls++
|
||||
emitter.once('value', listener).off('value', listener).emit('value')
|
||||
assert.equal(calls, 0)
|
||||
emitter.on('value', listener).on('value', listener).off('value', listener).emit('value')
|
||||
assert.equal(calls, 0)
|
||||
},
|
||||
'EVENT.throw-propagation': function (Emitter) {
|
||||
const emitter = new Emitter()
|
||||
const marker = new Error('listener failed')
|
||||
let later = false
|
||||
emitter.on('value', () => {
|
||||
throw marker
|
||||
}).on('value', () => {
|
||||
later = true
|
||||
})
|
||||
assert.throws(() => emitter.emit('value'), error => error === marker)
|
||||
assert.equal(later, false)
|
||||
},
|
||||
}
|
||||
@@ -1,24 +1,9 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the built-in runner without adding a test framework.
|
||||
import { mock, test } from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'vite'
|
||||
import { getViteBuildConfig } from '../scripts/utils.js'
|
||||
import { loadPackage } from './helpers/load.js'
|
||||
|
||||
const config = getViteBuildConfig({
|
||||
entry: fileURLToPath(new URL('../packages/artplayer-plugin-dash-control/src/index.js', import.meta.url)),
|
||||
name: 'artplayerPluginDashControl',
|
||||
format: 'es',
|
||||
fileName: 'index.js',
|
||||
minify: false,
|
||||
})
|
||||
config.build.write = false
|
||||
const result = await build(config)
|
||||
const outputs = (Array.isArray(result) ? result : [result]).flatMap(item => item.output)
|
||||
const { default: dashControl } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(outputs.find(item => item.type === 'chunk').code).toString('base64')}`,
|
||||
)
|
||||
const { default: dashControl } = await loadPackage('artplayer-plugin-dash-control')
|
||||
|
||||
function createPlayer() {
|
||||
const representations = [
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify actual JS/TS loading used by module regressions.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules, workspace } from './helpers/load.js'
|
||||
|
||||
test('Module fixtures load JS and TS without silently selecting an obsolete sibling', async () => {
|
||||
const parent = path.join(workspace, 'refactor/.cache')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
const directory = fs.mkdtempSync(path.join(parent, 'unit-loader-'))
|
||||
try {
|
||||
const ts = path.join(directory, 'double.ts')
|
||||
const js = path.join(directory, 'double.js')
|
||||
fs.writeFileSync(ts, 'export default (value: number): number => value * 2')
|
||||
assert.equal((await loadModules({ double: 'double' }, directory)).double(3), 6)
|
||||
fs.writeFileSync(js, 'export default value => value * 2')
|
||||
await assert.rejects(loadModules({ double: 'double' }, directory), /exactly one JS\/TS module/)
|
||||
fs.unlinkSync(ts)
|
||||
assert.equal((await loadModules({ double: 'double' }, directory)).double(3), 6)
|
||||
assert(!fs.existsSync(path.join(directory, 'dist')))
|
||||
}
|
||||
finally {
|
||||
const relative = path.relative(parent, directory)
|
||||
assert(relative.startsWith('unit-loader-') && !relative.includes(path.sep))
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import vm from 'node:vm'
|
||||
import { build as esbuild } from 'esbuild'
|
||||
import { build as viteBuild } from 'vite'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { getEntryFile } from '../../scripts/projects.js'
|
||||
import { getGlobalName, getViteBuildConfig } from '../../scripts/utils.js'
|
||||
|
||||
export const workspace = fileURLToPath(new URL('../../', import.meta.url))
|
||||
const importCode = code => import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
|
||||
export function resolveSource(name, root = workspace) {
|
||||
const files = ['.js', '.ts'].map(extension => path.resolve(root, name + extension)).filter(file => fs.existsSync(file))
|
||||
assert.equal(files.length, 1, `Expected exactly one JS/TS module: ${name}`)
|
||||
return files[0]
|
||||
}
|
||||
|
||||
export async function loadModules(exports, root = workspace) {
|
||||
const contents = Object.entries(exports).map(([alias, entry]) => {
|
||||
const { file, name = 'default' } = typeof entry === 'string' ? { file: entry } : entry
|
||||
assert(/^[\w$]+$/.test(alias) && /^[\w$]+$/.test(name), 'Invalid test export')
|
||||
return `export { ${name} as ${alias} } from ${JSON.stringify(resolveSource(file, root))};`
|
||||
}).join('\n')
|
||||
const { outputFiles } = await esbuild({ stdin: { contents, resolveDir: root }, bundle: true, write: false, platform: 'node', format: 'esm' })
|
||||
return importCode(outputFiles[0].contents)
|
||||
}
|
||||
|
||||
export async function loadPackage(name, root = workspace) {
|
||||
const project = path.join(root, 'packages', name)
|
||||
const config = getViteBuildConfig({ entry: getEntryFile(project), name: getGlobalName(name), format: 'es', fileName: 'index.js', minify: false })
|
||||
config.build.write = false
|
||||
const result = await viteBuild({ root: project, ...config })
|
||||
const chunks = (Array.isArray(result) ? result : [result]).flatMap(item => item.output).filter(item => item.type === 'chunk')
|
||||
assert.equal(chunks.length, 1, 'Source fixture loader requires one self-contained JS chunk')
|
||||
return importCode(chunks[0].code)
|
||||
}
|
||||
|
||||
function commonJs(code) {
|
||||
const module = { exports: {} }
|
||||
vm.runInNewContext(code, { module, exports: module.exports }, { timeout: 5000 })
|
||||
return module.exports
|
||||
}
|
||||
|
||||
export async function loadPublishedCore() {
|
||||
const release = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/releases.json'), 'utf8')).releases.find(item => item.name === 'artplayer')
|
||||
const archive = await ensureArchive(release)
|
||||
const member = 'package/dist/artplayer.js'
|
||||
const code = readMember(archive, member)
|
||||
assert.equal(hash(code), release.files[member], 'Published core member changed')
|
||||
return { name: `published ${release.version}`, Artplayer: commonJs(code.toString('utf8')) }
|
||||
}
|
||||
|
||||
export async function loadCoreArtifact(filename) {
|
||||
const absolute = path.resolve(filename)
|
||||
const Artplayer = absolute.endsWith('.mjs')
|
||||
? (await import(pathToFileURL(absolute))).default
|
||||
: commonJs(fs.readFileSync(absolute, 'utf8'))
|
||||
assert.equal(typeof Artplayer?.Emitter, 'function', 'Candidate artifact must expose Artplayer.Emitter')
|
||||
return { name: `candidate ${Artplayer.version} (${path.basename(absolute)})`, Artplayer }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mock } from 'node:test'
|
||||
|
||||
export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchMix }) {
|
||||
return function createArt(play = () => Promise.resolve(), playing = false) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
url: 'old.mp4',
|
||||
currentTime: 37,
|
||||
aspectRatio: '16:9',
|
||||
playbackRate: 1.5,
|
||||
notice: { show: '' },
|
||||
i18n: { get: key => key },
|
||||
option: { hotkey: true, mutex: false },
|
||||
constructor: { instances: [] },
|
||||
template: {
|
||||
$video: {
|
||||
paused: !playing,
|
||||
play: mock.fn(play),
|
||||
pause() { this.paused = true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Object.defineProperty(art, 'playing', { get: () => !art.template.$video.paused })
|
||||
for (const mix of [playMix, pauseMix, toggleMix, switchMix])
|
||||
mix(art)
|
||||
return art
|
||||
}
|
||||
}
|
||||
+12
-49
@@ -1,58 +1,21 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the built-in runner without adding a test framework.
|
||||
import { mock, test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
import { playbackFactory } from './helpers/playback.js'
|
||||
|
||||
// Bundle the source modules so tests also work with Node 20's ESM resolution.
|
||||
const { outputFiles } = await build({
|
||||
stdin: {
|
||||
contents: `
|
||||
export { silencePromise } from './packages/artplayer/src/utils/error.js';
|
||||
export { default as Emitter } from './packages/artplayer/src/utils/emitter.js';
|
||||
export { default as Hotkey } from './packages/artplayer/src/hotkey.js';
|
||||
export { default as playMix } from './packages/artplayer/src/player/playMix.js';
|
||||
export { default as pauseMix } from './packages/artplayer/src/player/pauseMix.js';
|
||||
export { default as toggleMix } from './packages/artplayer/src/player/toggleMix.js';
|
||||
export { default as switchMix } from './packages/artplayer/src/player/switchMix.js';
|
||||
`,
|
||||
resolveDir: fileURLToPath(new URL('../', import.meta.url)),
|
||||
},
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
const modules = await loadModules({
|
||||
silencePromise: { file: 'packages/artplayer/src/utils/error', name: 'silencePromise' },
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
Hotkey: 'packages/artplayer/src/hotkey',
|
||||
playMix: 'packages/artplayer/src/player/playMix',
|
||||
pauseMix: 'packages/artplayer/src/player/pauseMix',
|
||||
toggleMix: 'packages/artplayer/src/player/toggleMix',
|
||||
switchMix: 'packages/artplayer/src/player/switchMix',
|
||||
})
|
||||
const { silencePromise, Emitter, Hotkey, playMix, pauseMix, toggleMix, switchMix } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(outputFiles[0].contents).toString('base64')}`,
|
||||
)
|
||||
|
||||
function createArt(play = () => Promise.resolve(), playing = false) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
url: 'old.mp4',
|
||||
currentTime: 37,
|
||||
aspectRatio: '16:9',
|
||||
playbackRate: 1.5,
|
||||
notice: { show: '' },
|
||||
i18n: { get: key => key },
|
||||
option: { hotkey: true, mutex: false },
|
||||
constructor: { instances: [] },
|
||||
template: {
|
||||
$video: {
|
||||
paused: !playing,
|
||||
play: mock.fn(play),
|
||||
pause() { this.paused = true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Object.defineProperty(art, 'playing', { get: () => !art.template.$video.paused })
|
||||
for (const mix of [playMix, pauseMix, toggleMix, switchMix]) {
|
||||
mix(art)
|
||||
}
|
||||
return art
|
||||
}
|
||||
const { silencePromise, Hotkey } = modules
|
||||
const createArt = playbackFactory(modules)
|
||||
|
||||
test('silencePromise preserves fulfilled values and handles rejected promises', async () => {
|
||||
assert.equal(await silencePromise(Promise.resolve(42)), 42)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Run the same public contract against each actual implementation.
|
||||
import { test } from 'node:test'
|
||||
import { emitterContracts } from './contracts/emitter.js'
|
||||
import { loadCoreArtifact, loadPackage, loadPublishedCore } from './helpers/load.js'
|
||||
|
||||
const targets = [await loadPublishedCore(), { name: 'workspace', Artplayer: (await loadPackage('artplayer')).default }]
|
||||
if (process.env.ARTPLAYER_TEST_CORE)
|
||||
targets.push(await loadCoreArtifact(process.env.ARTPLAYER_TEST_CORE))
|
||||
|
||||
for (const { name, Artplayer } of targets) {
|
||||
for (const [id, verify] of Object.entries(emitterContracts))
|
||||
test(`${id}: ${name}`, () => verify(Artplayer.Emitter))
|
||||
}
|
||||
Reference in new issue
Block a user